Fundamentos de Python en el ámbito financiero

Laboratorio de Riesgos

David Angulo Duque

Introducción a la programación en Python

Agenda:

  1. Introducción a la programación en Python

  2. Python

  3. QuantLib

  4. Valoración de derivados financieros

  5. Modelo multifactorial de riesgo de crédito

¿Qué es un lenguaje programación?

Un lenguaje de programación es un conjunto formal de instrucciones que permite a los humanos comunicarse con las computadoras para resolver problemas mediante algoritmos.

Un lenguaje de programación actúa como un puente entre el pensamiento humano y la lógica de la máquina. Consiste en vocabulario (palabras reservadas como if, for), gramática (reglas de sintaxis) y semántica (significado de las instrucciones).

Las computadoras solo entienden lenguaje máquina (binario: 0s y 1s), por lo que los compiladores (programa queda listo para ejecutarse directamente en un procesador específico) o intérpretes (Traducción dinámica en tiempo de ejecución (Just-In-Time, JIT)) traducen el código fuente a instrucciones ejecutables.

Características principales de cualquier lenguaje de programación:

  • Sintaxis: Reglas que definen cómo escribir el código correctamente.
  • Semántica: Lo que significa cada instrucción válida.
  • Paradigmas: Estructural (secuencial), procedural, orientado a objetos, funcional.
  • Nivel: Bajo (cerca del hardware, como lenguaje de máquina o ensamblador) o alto (abstracción, como Python).

¿Qué es Python?

Python es un lenguaje de programación de alto nivel interpretado, multiparadigma y de propósito general creado a finales de los años 80 por Guido van Rossum. Se caracteriza por su énfasis en la legibilidad del código y la simplicidad de la sintaxis, convirtiendolo en una opción ideal tanto para principiantes como para desarrolladores experimentados.

  • De alto nivel: El lenguaje abstrae los detalles complejos de la máquina permitiendo que los programadores se concentren en resolver problemas específicos del dominio (en nuestro caso, finanzas).
  • Fácil de aprender: Su sintaxis simple permite escribir código más rápido y entenderlo mejor.
  • Interpretado: El código Python se ejecuta línea por línea a través de un intérprete, permitiendo desarrollo interactivo y prototipado rápido.
  • Dinámicamente tipado: Los tipos de datos se infieren en tiempo de ejecución, lo que reduce la verbosidad del código.
  • Multiparadigma: Soporta programación orientada a objetos, funcional, procedural e imperativa simultáneamente.
  • Multiplataforma: Funciona en Windows, macOS, Linux y otros sistemas operativos sin cambios en el código.
  • Código abierto: Es gratuito y cuenta con una gran comunidad que contribuye a su desarrollo.

El zen de Python

La filosofía de diseño de Python se resume en el “Zen de Python” (PEP 20), es un conjunto de 19 principios que guían el diseño del lenguaje Python y promueven la escritura de código limpio, legible y eficiente.

Pueden consultarse ejecutando el siguiente comando en cualquier intérprete Python:

import this

No osbstante, destacamos los siguientes:

  • “Beautiful is better than ugly”: El código bien escrito es más mantenible.
  • “Explicit is better than implicit”: La claridad es preferible a la magia.
  • “Simple is better than complex”: Las soluciones simples generalmente son mejores.
  • “Readability counts”: La legibilidad es crítica, el código es la documentación más actualizada y completa.

Deuda ténica

Metáfora usada en desarrollo de software para describir los costes futuros por tomar atajos o decisiones subóptimas (código sucio, mal estructurado,…) con el objetivo de acortar tiempos o entregar mas rápido.

Similar a una deuda financiera que acumula intereses, dificultando el mantenimiento y desarrollo futuro.

Comparativa con otros lenguajes

Aspecto Python R C++ Java Rust
% de uso1 57,9% 4,9% 23,5% 29.3% 14,8%
Tipo de Lenguaje Multiparadigma Funcional Multiparadigma Orientado a Objetos Multiparadigma
Compilado/Interpretado Interpretado Interpretado Compilado Compilado (bytecode) Compilado
Gestión de la memoria Automatico (GC) Automatico (GC) Manual Automatico (GC) Compilado (Ownership)
Tipado Dinámico Dinámico Estático Estático Estático
Curva de Aprendizaje Medio Medio Muy Alta Alta Muy Alta
Velocidad Ejecución Medio Medio Muy Rápido Rápido Muy Rápido
Velocidad Desarrollo Muy Rápido Rápido Lento Lento Lento
Año de Creación 1991 1995 1983 1995 2010

Ventajas y Desventajas de Python

Como Musashi (2018) enseña en El Libro de los Cinco Anillos, no existe el arma perfecta universal: cada una (espada, lanza, arco,…) brilla en su contexto específico. La maestría radica en adaptarse a las circunstancias y elegir la herramienta adecuada para cada situación.


Ventaja Impacto
Legibilidad Código fácil de enteder
Prototipado rápido Idea → Código en horas, no meses
Librerias disponibles Amplio abanico de utilidades ya escritas
Gratuito Sin coste
Producción De prototipos a producción
Proposito general Soluciones para casi cualquier problema
Desventaja Severidad
Velocidad Bucles lentos vs. C++ (1000x más lento)
Memoria Mayor consumo para datasets masivos
Latencia No apto para trading HFT (microsegundos)
GIL (Global Interpreter Lock) Multithread limitado en CPU
Producción crítica Necesita testing exhaustivo

“If you only have a hammer, everything looks like a nail”

¿Cómo programar en Python?

1. El intérprete

El componente central para programar en Python es el intérprete, que es el software que traduce y ejecuta tu código. Lo reconoceremos por los siguientes símbolos “>>>” o “…”

Instalar el interprete de Python

En Windows: Descarga el instalador ejecutable (.exe) a través de la página oficial

Durante la instalación, es importante añadir el directorio de instalación al “PATH”, lo que permite ejecutar Python desde cualquier terminal sin especificar la ruta completa. Una vez completada la instalación, verifica escribiendo en la línea de comandos:

python --version   # Versión del interprete
where python       # Ruta del intérprete

2. El IDE

Un editor de código o IDE es la herramienta donde escribirás tu código Python. Las opciones varían según tu nivel y necesidades:

Existen otros: Visual Studio, Jupiter, Spyder

3. El gestor de paquetes

pip (Package Installer for Python) es la herramienta estándar para instalar librerías y paquetes adicionales. Viene incluido automáticamente con Python

python -m pip install numpy pandas
pip install numpy pandas
pip list

Al ejecutar el comando pip install el gestor de paquetes pip buscará en los repositorios públicos, si existe lo descargará, junto a sus dependencias.

Python

Agenda:

  1. Introducción a la programación en Python

  2. Python

  3. QuantLib

  4. Valoración de derivados financieros

  5. Modelo multifactorial de riesgo de crédito

Intérprete de Python

Programa que lee, traduce y ejecuta el código línea por línea en tiempo real, actuando como puente entre tu lógica y la máquina

También conocido como REPL (Read-Eval-Print Loop), opera en un ciclo continuo: lee tu código, lo evalúa, imprime el resultado y espera la siguiente instrucción. Cuando ejecutas python en la terminal, inicia una sesión interactiva que muestra el prompt primario >>> para comandos nuevos y el prompt secundario ... para líneas continuas, como bucles o funciones.

$ python
Python 3.12.3 (main, Jun 18 2025, 17:59:45) [GCC 13.3.0] on linux
Type "help", "copyright", "credits" or "license" for more information.
>>> quit()

El intérprete muestra información de versión y copyright, luego el prompt >>>. Para salir, usa: quit(), exit() o Ctrl+C (Linux/macOS) / Ctrl+Z (Windows).

  • Lectura (Read): Captura tu línea de código.
  • Evaluación (Eval): Compila a bytecode y ejecuta si el código es válido, si no lanzará una excepción.
  • Impresión (Print): Muestra el resultado.
  • Bucle (Loop): Vuelve al prompt >>>.

Durante la evaluación el intérprete ignora los denominados comentarios, son anotaciones en el código para documentar, explicar lógica o desactivar temporalmente líneas sin generar errores. Se crean con el símbolo # al inicio de una línea o después del código. Para bloques multilínea, se usan comillas triples (’’’ o “““) sin asignar a variable

# Esto es un comentario ignorado por Python
print("Hola Mundo") # A partir de '#' el interprete no lo procesa
'''
Este es un comentario multinea:
Los comentarios mejoran la legibilidad y mantenimiento del código.
Las buenas prácticas  recomiendan comentarios concisos, actualizados y enfocados en el "porqué" más que en el "qué".
'''

Operadores matemáticos

El intérprete actua como una simple calculadora. Por ejemplo:

2 + 2 
50 - 5 * 6
(50 - 5 * 6) / 4
5 ** 2
17 / 3  # division clasiva 
17 // 3  # operador //: cociente sin la parte decimal
17 % 3  # operador %: modulo
5 * 3 + 2  # cociente * divisor + resto
width = 20
height = 5 * 9
width * height
x, y = 10, 20 # Asignación múltiple
a = b = c = 0 # Dar mismo valor a varias variables


Operadores suma, resta, multiplicación y división

Podemos ejecutar cualquier operación (+, -,*, /) y nos devolverá el resultado. Además podemos usar paréntesis (()) para agrupar operaciones

\[ \begin{align} 2 + 2 &= 4 \\ 50 - 5 · 6 &= 20 \\ \frac{(50 - 5 · 6)}{4} &= 5 \\ \end{align} \]

Operador potencia

Podemo usar el operador ** para calcular potencias, por ejemplo:

\[ 5^2 = 25 \]

Operadores división, cociente y módulo

La divisíon cĺasica /, el cociente entero //, operador módulo %:

\[ \begin{align} \frac{17}{3} &= 5.666666666666667 \\ 17 \lfloor 3 &= 5 \\ 17 \pmod{3} &= 2 \\ 5 · 3 + 2 &= 17 \end{align} \]

Operador igual: asignación de variable

El signo igual (=) se usa para asignar un valor a una variable. Cuando asignamos una o múltiples variables, el interprete no mostrará ningun resultado, antes del siguiente prompt interactivo:

No obstante, existen 35 términos predefinidos que el intérprete reserva exclusivamente para su sintaxis y control de flujo, impidiendo su uso como nombres de variables, funciones o identificadores para evitar conflictos. Pueden ser consultadas ejecutando el siguiente comando:

import keyword
keyword.kwlist

Comparación y operadores lógicos

Hay tres operadores lógicos en Python: and, or y not, sirven para evaluar expresiones booleanas y permiten combinar condiciones en estructuras de control como if o bucles.

Operador and (&)

Devuelve True solo si ambos operandos son verdaderos, evaluación en cortocircuito. Ejemplo: True and False resulta en False.

Operador or (|)

Retorna True si al menos un operando es verdadero, también con cortocircuito. Ejemplo: True or False devuelve True.

Operador not (!)

Invierte el valor booleano del operando: not True es False, y viceversa; tiene mayor precedencia que and y or (orden: not > and > or). ​ Los operadores de comparación en Python, como “greater than” (>), complementan a los lógicos al generar valores booleanos para condiciones.

Operadores básicos

  • \(>\) (mayor que): 5 > 3 es True.
  • \(<\) (menor que): 3 < 5 es True.
  • \(>=\) (mayor o igual): 5 >= 5 es True.
  • \(<=\) (menor o igual): 3 <= 5 es True. ​

Operadores de Igualdad

  • \(==\) (igual): 5 == 5 es True.
  • \(!=\) (diferente): 5 != 3 es True.

Uso Combinado

Se integran con and, or y not en expresiones como if x > 10 and y <= 20:, evaluando precedencia de izquierda a derecha con cortocircuito.

# La expresión if en Python permite condicionar la ejecucion del programa.
# Ejecuta el codigo si la condicion se cumple, si no ejecuta la sentencia despues del else
# Se puede encadenar con if - elif - else
nota = 75
print("Aprobado") if nota >= 50 else print("Suspenso")

Tipos de datos: básicos

Como dijimos anteriormente, Python es un lenguaje dinamicante tipado, el intérprete se encarga de inferir el tipo del objeto en tiempo de ejecución. Al contrario que en los lenguajes compilados como C++ o Rust en los que generalemente su tipado es estático (definido por el desarrollador) o inferido en tiempo de compilación. Ver (Python Software Foundation, 2025) para tipos de datos mas espcializados.

Tipo Significado Uso
int Integer Números naturales
float Número con punto flotante Números reales
bool Booleano Valores binarios (Verdadero/Falso)
str String Caracteres, palabras, texto, …

Integer (int) - representa números enteros, valores numéricos sin parte decimal, pueden ser positivos o negativos. En Python 3 los enteros tienen precisión arbitraria; no tienen un límite de tamaño fijo (overflow).

  • Contadores e índices: Controlar bucles (for, while) y acceder a posiciones en listas o bases de datos.
  • Matemáticas discretas: Representar cantidades indivisibles (ej. número de acciones, personas, artículos en inventario).

Tipos de datos: básicos

Como dijimos anteriormente, Python es un lenguaje dinamicante tipado, el intérprete se encarga de inferir el tipo del objeto en tiempo de ejecución. Al contrario que en los lenguajes compilados como C++ o Rust en los que generalemente su tipado es estático (definido por el desarrollador) o inferido en tiempo de compilación. Ver (Python Software Foundation, 2025) para tipos de datos mas espcializados.

Tipo Significado Uso
int Integer Números naturales
float Número con punto flotante Números reales
bool Booleano Valores binarios (Verdadero/Falso)
str String Caracteres, palabras, texto, …

Número con punto flotante (float) - se utiliza para representar números reales con parte decimal. En Python, estos se implementan generalmente como valores de “doble precisión”.

  • Cálculos matematicos y estadísticos: Medidas continuas como temperatura, distancias, pesos o probabilidades.
  • Representación de magnitudas: precios, tasas de interés o porcentajes.

Tipos de datos: básicos

Como dijimos anteriormente, Python es un lenguaje dinamicante tipado, el intérprete se encarga de inferir el tipo del objeto en tiempo de ejecución. Al contrario que en los lenguajes compilados como C++ o Rust en los que generalemente su tipado es estático (definido por el desarrollador) o inferido en tiempo de compilación. Ver (Python Software Foundation, 2025) para tipos de datos mas espcializados.

Tipo Significado Uso
int Integer Números naturales
float Número con punto flotante Números reales
bool Booleano Valores binarios (Verdadero/Falso)
str String Caracteres, palabras, texto, …

Booleano (bool) - solo puede tomar dos valores: True (Verdadero) o False (Falso). Es una subclase de int en Python (donde True se comporta como 1 y False como 0 en contextos aritméticos).

  • Control de flujo: Determinar qué camino toma el código en sentencias if, while o filtros de datos.
  • Indicar estados binarios (ej. is_active, has_error, market_open).
  • Validaciones: Resultados de comparaciones (ej. precio > 100 devuelve un bool).

Tipos de datos: básicos

Como dijimos anteriormente, Python es un lenguaje dinamicante tipado, el intérprete se encarga de inferir el tipo del objeto en tiempo de ejecución. Al contrario que en los lenguajes compilados como C++ o Rust en los que generalemente su tipado es estático (definido por el desarrollador) o inferido en tiempo de compilación. Ver (Python Software Foundation, 2025) para tipos de datos mas espcializados.

Tipo Significado Uso
int Integer Números naturales
float Número con punto flotante Números reales
bool Booleano Valores binarios (Verdadero/Falso)
str String Caracteres, palabras, texto, …

String (str) - Es una secuencia inmutable de caracteres. Se delimitan con comillas simples (’ ’) o dobles (” “). Al ser inmutables, no se pueden modificar en el mismo espacio de memoria una vez creados; cualquier”cambio” genera una nueva cadena.​​

  • Procesamiento de datos: Almacenamiento de nombres, direcciones, códigos, y lectura de archivos CSV/JSON.
  • Interacción con usuario: Mensajes mostrados en pantalla o logs de errores.
  • Identificadores: Claves en diccionarios o bases de datos no numéricas.

Tipos de datos: básicos

1. None (Valores nulos u opcionales)

None es el objeto singleton que utiliza Python para representar la ausencia de valor o un estado nulo. Se usa para inicializar variables vacías o como valor por defecto en funciones. Se compara con is no con ==

x = None
if x is None:
    print("x no tiene valor")


2. type()

La función type(obj) devuelve la clase exacta del objeto obj

x = 1
type(x) # <class 'int'>


3. Conversion de tipos (Casting)

Usamos los constructores de clase para convertir entre tipos: int(), float(), str(), list().

Python es fuertemente tipado pero dinámico. No hace “casting implícito” que pierda datos (como sumar “5” + 5 dará error), por lo que debemos convertir explícitamente.

precio = "50"
total = int(precio) + 10  # Conversión explícita de str a int
precio + 10 # error

Estructuras de datos (Colecciones)

Son estructuras de datos que permiten almacenar y organizar múltiples elementos, agrupándolos en tipos especializados. Todas admiten elementos heterogéneos, son iterables e indexables (excepto sets), y ofrecen métodos optimizados para agregar, eliminar o buscar datos según su naturaleza mutable/inmutable. Ver (Python Software Foundation, 2025).

Tipo Mutable / Inmutable Uso
list Mutable Conjunto variable de objetos
tuple Inmutable Conjunto fijo de objetos
dict Mutable Almacenamiento clave-valor
set Mutable Conjunto: Colección de objetos únicos

Lista (list) - Secuencias mutables y ordenadas, definidas con [] o list(), usadas para datos modificables. Pueden contener objetos de distintos tipos (enteros, strings, incluso otras listas) mezclados.

  • Mantienen el orden de inserción.
  • Permiten elementos duplicados.
  • Sus elementos pueden modificarse, añadirse o eliminarse dinámicamente.

Estructuras de datos (Colecciones)

Son estructuras de datos que permiten almacenar y organizar múltiples elementos, agrupándolos en tipos especializados. Todas admiten elementos heterogéneos, son iterables e indexables (excepto sets), y ofrecen métodos optimizados para agregar, eliminar o buscar datos según su naturaleza mutable/inmutable. Ver (Python Software Foundation, 2025).

Tipo Mutable / Inmutable Uso
list Mutable Conjunto variable de objetos
tuple Inmutable Conjunto fijo de objetos
dict Mutable Almacenamiento clave-valor
set Mutable Conjunto: Colección de objetos únicos

Tupla (tuple) - Secuencias inmutables y ordenadas, definidas con () o tuple(). Una vez creada, no puedes añadir, borrar ni cambiar sus elementos. Esto las hace más ligeras en memoria y seguras contra modificaciones accidentales.

  • Son más rápidas de procesar que las listas.
  • Se pueden usar como claves en diccionarios (las listas no, al ser mutables).

Estructuras de datos (Colecciones)

Son estructuras de datos que permiten almacenar y organizar múltiples elementos, agrupándolos en tipos especializados. Todas admiten elementos heterogéneos, son iterables e indexables (excepto sets), y ofrecen métodos optimizados para agregar, eliminar o buscar datos según su naturaleza mutable/inmutable. Ver (Python Software Foundation, 2025).

Tipo Mutable / Inmutable Uso
list Mutable Conjunto variable de objetos
tuple Inmutable Conjunto fijo de objetos
dict Mutable Almacenamiento clave-valor
set Mutable Conjunto: Colección de objetos únicos

Diccionario (dict) - Pares clave-valor no ordenados, como {"nombre": "Ana", "edad": 25}, definidas como: {clave: valor} o dict():

  • Las claves deben ser únicas e inmutables (strings, números, tuplas).
  • Acceso extremadamente rápido a los valores (complejidad O(1) promedio).

Estructuras de datos (Colecciones)

Son estructuras de datos que permiten almacenar y organizar múltiples elementos, agrupándolos en tipos especializados. Todas admiten elementos heterogéneos, son iterables e indexables (excepto sets), y ofrecen métodos optimizados para agregar, eliminar o buscar datos según su naturaleza mutable/inmutable. Ver (Python Software Foundation, 2025).

Tipo Mutable / Inmutable Uso
list Mutable Conjunto variable de objetos
tuple Inmutable Conjunto fijo de objetos
dict Mutable Almacenamiento clave-valor
set Mutable Conjunto: colección de objetos únicos

Conjuntos (set) - Estructuras de datos no ordenadas sin duplicados ni indexación, definidas con {valor1, valor2} o set(), útiles para operaciones únicas.

  • Elimina duplicados.
  • Altamente optimizado para operaciones matemáticas de conjuntos (unión, intersección, diferencia).
  • Acceso rápido para verificar pertenencia (if x in mi_set).

Blucles (Loops)

Los loops (bucles) son estructuras de control que permiten ejecutar un bloque de código múltiples veces, iterando sobre secuencias o bajo condiciones específicas los principales son: for y while.

Loop for

Permite especificar de antemano el número de iteraciones. Se define con for seguido de la variable receptora, la palabra in, la secuencia iterable, dos puntos : y el cuerpo indentado del bloque a ejecutar.

# Iterar sobre lista
frutas = ["manzana", "plátano", "naranja"]
for it_fruta in frutas:
    print(it_fruta)

# Iterar sobre rango
for it_range in range(5):  # 0, 1, 2, 3, 4
    print(f"Número: {it_range}")

# Iterar sobre string
for it_letra in "Python":
    print(it_letra)

# Iterar con índice
for it_indice, it_fruta in enumerate(frutas):
    print(f"{it_indice}: {it_fruta}")

# Iterar sobre múltiples secuencias simultáneamente
nombres = ["Ana", "Bob", "Carlos"]
edades = [25, 30, 35]
for it_nombre, it_edad in zip(nombres, edades):
    print(f"{nombre} tiene {edad} años")
# Crear lista de cuadrados
[x**2 for x in range(5)] # [0, 1, 4, 9, 16]

# Con condición
[x for x in range(10) if x % 2 == 0] # [0, 2, 4, 6, 8]

# Comprensión de diccionario
{x: x**2 for x in range(4)} # {0: 0, 1: 1, 2: 4, 3: 9}

# Iterar sobre diccionarios
persona = {"nombre": "Ana", "edad": 25, "ciudad": "Madrid"}

# Iterar sobre claves
for it_clave in persona:
    print(it_clave)

# Iterar sobre valores
for it_valor in persona.values():
    print(it_valor)

# Iterar sobre pares clave-valor
for it_clave, it_valor in persona.items():
    print(f"{it_clave}: {it_valor}")

Blucles (Loops)

Los loops (bucles) son estructuras de control que permiten ejecutar un bloque de código múltiples veces, iterando sobre secuencias o bajo condiciones específicas los principales son: for y while.

Loop while

Permite ejecutar un bloque de código repetidamente mientras se cumpla una condición específica. Se define con while seguido de la condición booleana, dos puntos : y el cuerpo indentado que se repite hasta que la condición sea falsa.

# Contador simple
contador = 0
while contador < 5:
    print(f"Contador: {contador}")
    contador += 1

# Validación de entrada
mientras = True
while mientras:
    respuesta = input("¿Continuar? (s/n): ")
    if respuesta.lower() == 'n':
        mientras = False

Controles de Flujo

break: Termina el loop prematuramente:

for i in range(10):
    if i == 5:
        break  # Sale del loop
    print(i)  # Output: 0, 1, 2, 3, 4

continue: Salta la iteración actual y continúa con la siguiente:

for i in range(5):
    if i == 2:
        continue  # Salta i=2
    print(i)  # Output: 0, 1, 3, 4

else: Se ejecuta cuando el loop termina sin break:

for i in range(5):
    print(i)
else:
    print("Loop completado sin break")

Funciones

Las funciones son bloques de código que pueden ser reutilizados y que realizan una tarea específica, pueden recibir parámetros como entrada y pueden devolver resultados. Las ventajas de usar funciones son: reutilización (escribir código una sola vez, usarlo muchas veces), modularidad (dividir problemas complejos en tareas simples), mantenibilidad (cambios centralizados en una ubicación) y legibilidad (código más organizado y fácil de entender).

Una función se define con la palabra reservada def seguida del nombre de la función, paréntesis con parámetros opcionales y dos puntos, iniciando un bloque indentado con la lógica a ejecutar.

Componentes:

  • Nombre: identificador único de la función (ej: sumar)
  • Parámetros: variables de entrada (ej: a, b)
  • Docstring: (Opcional) descripción entre triples comillas “““…”“”
  • Cuerpo: lógica que ejecuta la función
  • Return: termina la ejecución, y define el valor devuelto (devuelve None si no se devuelve nada)
def sumar(a, b):
    c = a + b
    return c

print(sumar(5, 3))  # Output: 8

Otra forma de definirlas es a través de funciones anónimas: funciones sin nombre que se define en una sola línea usando la palabra reservada lambda.

sumar = lambda x, y: x + y

Parametros y argumentos

Los parámetros de una función son variables que reciben valores (argumentos) cuando se invoca, permitiendo que la función procese datos de entrada y realice operaciones específicas sobre ellos.

Tipos de parámetros

  • Posicionales: Se asignan por orden de aparición; el orden importa
  • Valores por defecto: Tienen un valor predefinido si no se proporciona argumento
  • Nombrados: Se pasan especificando el nombre del parámetro, ignorando el orden:
def saludar(nombre, mensaje="Hola"):
    return f"{mensaje}, {nombre}"

print(saludar("Ana"))                    # Posicional + valor por defecto - Output: Hola, Ana
print(saludar("Ana", "Buenos días"))     # Posicional sin valor por defecto - Output: Buenos días, Ana
print(saludar(mensaje="Buenos días", nombre="Ana")) # Nombrados
  • *args (Argumentos Variables Posicionales): Acepta cualquier número de argumentos posicionales como tupla:
def suma_todo(*numeros):
    return sum(numeros)

print(suma_todo(1, 2, 3, 4, 5))  # Output: 15
  • ** kwargs (Argumentos Variables Nombrados) : Acepta cualquier número de argumentos clave-valor como diccionario:
def mostrar_info(**datos):
    for clave, valor in datos.items():
        print(f"{clave}: {valor}")

mostrar_info(nombre="Ana", edad=30, profesión="Ingeniera")

Ejercicio: Capitalización

  1. Dado un capital incial (\(C\)), una tasa interes anual (\(r\)) y el tiempo de la inversion en años (\(t\)). Implementa la lógica para calcular el Valor Futuro (\(FV\)) usando un tipo de capitalización: interés simple, compuesto o continuo (opcional), utilizando las siguientes fórmulas:
  • Interés simple: \(FV = C · (1 + r·t)\)
  • Interés compuesto: \(FV = C · (1 + r)^t\)
  • Interes continuo: \(FV = C · e ^ {rt}\)


def calcular_valor_futuro(capital_inicial, tasa, t, tipo_capitalizacion):
    ...

calcular_valor_futuro(100, 0.01, 5, "simple")
calcular_valor_futuro(100, 0.01, 5, "compuesto")

Ejercicio: Capitalización

  1. Dado un capital incial (\(C\)), una tasa interes anual (\(r\)) y el tiempo de la inversion en años (\(t\)). Implementa la lógica para calcular el Valor Futuro (\(FV\)) usando un tipo de capitalización: interés simple, compuesto o continuo (opcional), utilizando las siguientes fórmulas:
  • Interés simple: \(FV = C · (1 + r·t)\)
  • Interés compuesto: \(FV = C · (1 + r)^t\)
  • Interes continuo: \(FV = C · e ^ {rt}\)


numero_e = 2.718281828459045

def calcular_valor_futuro(capital_inicial, tasa, t, tipo_capitalizacion):
    if tipo_capitalizacion == "simple":
        return capital_inicial * (1 + tasa * t)
    elif tipo_capitalizacion == "compuesto":
        return capital_inicial * (1 + tasa) ** t
    elif tipo_capitalizacion == "continuo":
        # Alternativamente math.exp() para e^(x) importando math: import math
        return capital_inicial * (numero_e ** (tasa * t))
    else:
        print("Opción no válida.")
    
    return

calcular_valor_futuro(100, 0.01, 5, "simple")
calcular_valor_futuro(100, 0.01, 5, "compuesto")
calcular_valor_futuro(100, 0.01, 5, "continuo")

Ejercicio: Serie de Fibonacci

Escribir una funcion que genere el numero \(i\) de la serie de Fibonacci.

En la serie de Fibonacci el número \(i\) es la número es la suma de los dos anteriores.

  • La secuencia comienza con los números 0 y 1. Es decir, \(F(0) = 0\) y \(F(1) = 1\)

  • A partir de ahí, el siguiente número se calcula sumando los dos anteriores:

    \(F_i = F_{i−1} + F_{i−2}\)

  • Serie de Fibonnaci: \(0, 1, 1, 2, 3, 5, 8, 13, 21, 34...\)

def fibonacci(...):
    ...

for it in range(0, 100):
    print(fibonacci(it))

Ejercicio: Serie de Fibonacci

Escribir una funcion que genere el numero \(i\) de la serie de Fibonacci.

En la serie de Fibonacci el número \(i\) es la número es la suma de los dos anteriores.

  • La secuencia comienza con los números 0 y 1. Es decir, \(F(0) = 0\) y \(F(1) = 1\)

  • A partir de ahí, el siguiente número se calcula sumando los dos anteriores:

    \(F_i = F_{i−1} + F_{i−2}\)

  • Serie de Fibonnaci: \(0, 1, 1, 2, 3, 5, 8, 13, 21, 34...\)

# Opcion a:
def fibonacci(i):
    if i == 0: return 0
    if i == 1: return 1
    
    a, b = 0, 1
    
    for it in range(2, i + 1):
        a = b 
        b = a + b

    return b

# Opcion b:
def fibonacci(i):
    if i == 0: return 0
    if i == 1: return 1

    resultado = fibonacci(i - 1) + fibonacci(i - 2)

    return resultado

for it in range(0, 100):
    print(fibonacci(it))

Aunque la definición matemática es recursiva (\(F(i)=F(i−1)+F(i−2)\)), programarla con un bucle (for) tiene complejidad lineal \(O(n)\).

Si usaramos recursión simple:

return fibonacci(i-1) + fibonacci(i-2)

la complejidad es exponencial, haciendo que calcular el término 50 tarde mucho tiempo, ya que reevaluamos lo evaluado.

Ejercicio: Serie de Fibonacci

Escribir una funcion que genere el numero \(i\) de la serie de Fibonacci.

En la serie de Fibonacci el número \(i\) es la número es la suma de los dos anteriores.

  • La secuencia comienza con los números 0 y 1. Es decir, \(F(0) = 0\) y \(F(1) = 1\)

  • A partir de ahí, el siguiente número se calcula sumando los dos anteriores:

    \(F_i = F_{i−1} + F_{i−2}\)

  • Serie de Fibonnaci: \(0, 1, 1, 2, 3, 5, 8, 13, 21, 34...\)

def fibonacci(i, cache = {0: 0, 1: 1}):
    if i in cache:
        return cache[i]

    resultado = fibonacci(i - 1, cache) + fibonacci(i - 2, cache)
    
    cache[i] = resultado
    
    return resultado

for it in range(0, 100):
    print(fibonacci(it))

Esta técnica se conoce técnicamente como Memoización (Memoization). Al almacenar los resultados previos, transformamos un algoritmo de complejidad exponencial \(O(2^n)\) a uno lineal \(O(n)\), haciendo viable la recursividad para números grandes.

Ejercicio: Integral numerica

Escribe un programa en Python que calcule numericamente la integral definida de la función \(f(x) = x ^ 2 · sin(x)\) en el intervalo [a, b]:

\[ \int^a_b x^2 · sin(x) dx \]

En muchos problemas de finanzas, nos encontramos con funciones que son difíciles o imposibles de integrar analíticamente (con lápiz y papel). En estos casos, aproximamos el área bajo la curva dividiendo el intervalo en pequeños rectángulos (Suma de Riemann). El ancho de todas las barras sera igual mientras que a altura de cada barra la determina el valor de la función en ese punto.

  1. Definir la función en python
  2. Implementar la función integral_riemann(f, a, b, n)

Algoritmo (Suma del area de rectángulos):

  1. Calcula el ancho de cada rectángulo: \(\Delta x= \frac{b-a}{n}\).
  2. Itera n veces. En cada paso:
  1. Calcula la coordenada x del lado izquierdo del rectangulo: \(x_i=a+i·\Delta x\).
  2. Calcula la altura del rectángulo evaluando la función: \(f(x_i)\).
  3. Suma el área del rectangulo (\(base · altura\)).

Ejercicio: Integral numerica

import math

def f(x):
    return (x**2) * math.sin(x)

def integral_izquierda(a, b, n):
    """
    Calcula la integral usando Suma de Riemann (Extremo Izquierdo).
    La altura del rectángulo se define por el valor de f(x) 
    al inicio (izquierda) de cada sub-intervalo.
    """
    ancho = (b - a) / n
    suma_areas = 0.0
    
    for i in range(n):
        # Usamos el borde izquierdo del rectángulo 'i'
        x = a + ancho * i
        
        altura = f(x)
        suma_areas += ancho * altura
        
    return suma_areas

a = 0
b = math.pi
valor_real = 5.869604401 # (pi^2 - 4)

integral_izquierda(a, b, 10)
integral_izquierda(a, b, 100)
integral_izquierda(a, b, 1000)
integral_izquierda(a, b, 1000000)

Librerias y módulos

Las librerías y módulos son colecciones de código reutilizable que agrupan funciones, clases y variables relacionadas, permitiendo extender la funcionalidad del lenguaje sin reescribir código desde cero.

Módulo

Archivo .py que contiene código Python (funciones, clases, variables) que puede importarse a través de la palabra reservado import y reutilizarse en otros programas:

# archivo: matematica.py
def sumar(a, b):
    return a + b

def restar(a, b):
    return a - b

PI = 3.14159

Librería (Package)

Colección organizada de módulos en directorios con estructura jerárquica, facilitando la gestión de proyectos grandes.

# Importar Módulo Completo
import matematica
resultado = matematica.sumar(5, 3)  # Output: 8


# Importar Funciones Específicas
from matematica import sumar, PI

resultado = sumar(10, 5)  # Output: 15
print(PI)                 # Output: 3.14159


# Importar con Alias
import matematica as mat
from mat import sumar as suma_numeros

resultado = suma_numeros(7, 2)


# Importar todo
import * from matematica

resultado = sumar(7, 2)

Librerias estándar principales

math

Operaciones matemáticas como raíces cuadradas (sqrt()), funciones trigonométricas (sin(), cos(), tan()), logaritmos, exponenciales y constantes matemáticas fundamentales como pi y e, además de funciones para números enteros como factorial() y gcd().

import math
print(math.sqrt(16))      # 4.0
print(math.pi)            # 3.14159...
print(math.factorial(5))  # 120


random

Genera números pseudoaleatorios, incluyendo enteros (randint()), flotantes uniformes (random()), selección aleatoria de elementos (choice()), muestreo (sample()) y mezclado de listas (shuffle()).

import random
print(random.randint(1, 10))      # Entero aleatorio
print(random.choice([1, 2, 3]))   # Elemento aleatorio
print(random.shuffle([1, 2, 3]))  # Mezcla lista

datetime

Fechas, horas y duraciones, permitiendo crear objetos de fecha/hora (datetime.now()), calcular diferencias (timedelta), formatear fechas (strftime()), instanciar fechas a partir de strings (strptime()) y realizar operaciones de calendario.

from datetime import datetime, timedelta
ahora = datetime.now()
print(ahora)
mañana = ahora + timedelta(days=1)


os

Ofrece interacción con el sistema operativo para operaciones de archivos y directorios como obtener el directorio actual (getcwd()), listar contenido (listdir()), crear/eliminar carpetas (mkdir(), rmdir()) y navegar rutas (path.join()).

import os
print(os.getcwd())        # Directorio actual
os.mkdir("nueva_carpeta") # Crear carpeta

Clases

Las clases en Python son plantillas para crear objetos con el objetivo de extender los tipos básicos que contienen atributos (datos) y métodos (funciones). Es un paradigma de programación que permite encapsular y representar nuestro problemas en unidades lógicas.

Cuando definimos una clase, el término self hace referencia al objeto actual, permitiendo acceder a sus atributos y métodos sin estar instanciado.


class Persona:
    """Clase que representa una persona"""
    
    def __init__(self, nombre, edad):
        """Constructor: inicializa atributos"""
        self.nombre = nombre
        self.edad = edad
    
    def saludar(self, apellido = ""):
        """Método: función dentro de la clase"""
        return f"Hola, soy {self.nombre} {apellido}"

# Crear objeto (instancia)
persona1 = Persona("Ana", 25)
print(persona1.saludar("Martinez")) 
# Output: Hola, soy Ana Martinez

Definición de la clase

Una clase se define con la palabra reservada class seguido del nombre, :, y a continuación se declaran los atributos y métodos usando la indentación:

Atributos

Variables que pertenecen a la clase, almacenando datos del objeto:

Podemos acceder a sus atributos a través de self.nombre desde dentro de la clase o usando . despues del objeto instanciado objeto.nombre desde fuera de la clase.

Métodos

Son funciones que “viven” dentro de la clase. Permite que los objetos no solo almacenen datos (atributos), sino que también hagan cosas con ellos.

De igual manera que los atributos, podemos acceder a sus atributos a través de self.saludar() desde dentro de la clase o usando . despues del objeto instanciado objeto.saludar() desde fuera de la clase.

Constructor (init)

El constructor es un método especial que se ejecuta al crear una instancia, inicializando atributos.

Se declara usando def __init__():. Y define como se va a instanciar el objeto y los parametros que necesita.

Ventajas de las Clases

  • Encapsulación: Agrupar datos y funcionalidad relacionada
  • Reutilización: Crear múltiples objetos de la misma clase
  • Organización: Código más legible y mantenible
  • Herencia: Las subclases heredan propiedades de clases padre

Ejercicio: Riesgo de crédito

Implementar una clase que represente una única exposición crediticia y encapsule la lógica para calcular la pérdida esperada y capital regulatorio (fórmula de Vasicek).

Inicializar los 4 parámetros de riesgo como atributos del objeto.:

  • ead: Exposición en Default.
  • pd: Probabilidad de incumplimiento.
  • lgd: Severidad.
  • rho: Correlación de activos.

Métodos:

\[ \text{Perdida Esperada} = EAD · LGD · PD \]

\[ Capital = LGD · \left( \Phi \left(\frac{\Phi^{-1}(PD) + \sqrt\rho \Phi^{-1}(\alpha)}{\sqrt{1 - \rho}} \right) - PD\right) \]

donde: \(\alpha\) es el nivel de confianza (por defecto, 99.9%), \(\Phi\) es la distribución normal estándar acumulada norm.cdf) de la libreria from scipy.stats import norm y \(\Phi^{-1}\) es la inversa de la normal estándar norm.cdf.

Ejercicio: Riesgo de crédito

from scipy.stats import norm
import math

class Posicion:
    def __init__(self, ead, pd, lgd, rho):
        self.ead = float(ead)
        self.pd = float(pd)
        self.lgd = float(lgd)
        self.rho = float(rho)

    def calcular_el(self):
        return self.ead * self.pd * self.lgd

    def calcular_capital(self, confianza=0.999):
        if self.pd == 0 or self.pd == 1 or self.rho == 1:
            return 0.0

        # Componentes de la fórmula de Vasicek
        inv_pd = norm.ppf(self.pd)          # N^-1(PD)
        inv_conf = norm.ppf(confianza)      # N^-1(0.999)
        
        numerador = inv_pd + (math.sqrt(self.rho) * inv_conf)
        denominador = math.sqrt(1 - self.rho)
        
        # PD condicional
        pd_cond = float(norm.cdf(numerador / denominador))
        
        # Capital Rate = LGD * (PD_cond - PD_uncond)
        k_rate = self.lgd * (pd_cond - self.pd)
        
        return self.ead * k_rate


prestamo = Posicion(ead=1_000_000, pd=0.02, lgd=0.45, rho=0.15)
    
el = prestamo.calcular_el()
capital = prestamo.calcular_capital()

Ejercicio: Bono cupo cero

Implementar la clase de un bono cupón cero

Inicializar dos atributos:

  • nominal: El valor monetario del bono al vencimiento.
  • maturity_date: Un objeto de tipo datetime que representa la fecha de vencimiento.

Valor del bono recibe:

  • rate: La tasa de interés anual de mercado.
  • valuation_date: (Opcional) La fecha en la que se quiere valorar el bono.

\[ NPV = \frac{Nominal}{(1 + rate) ^ {\frac{\text{maturity date} - \text{valuation date}}{365} }} \]

Ejercicio: Bono cupo cero

from datetime import datetime
import math

class ZeroCouponBond:
    def __init__(self, nominal, maturity_date):
        self.nominal = float(nominal)
        self.maturity = maturity_date

    def npv(self, rate, valuation_date=None):
        # 1. Determinar fecha de valoración
        val_date = datetime.now() if valuation_date is None else valuation_date

        if not isinstance(val_date, datetime):
            return 0

        # si ya venció, el valor presente es 0 (o ya se pagó)
        if val_date >= self.maturity:
            return 0.0

        dias_a_vencimiento = (self.maturity - val_date).days
        T = dias_a_vencimiento / 365.0
        
        # NPV = N / (1 + r)^(T)
        return self.nominal / ((1 + rate) ** T)


bono = ZeroCouponBond(1000, datetime(2030, 12, 31))

bono.npv(0.05)
bono.npv(0.05, datetime(2023, 12, 31))

Herencias

La herencia es un mecanismo que permite definir una nueva clase a partir de otra existente, de forma que la clase derivada reutiliza, amplía o modifica los atributos y métodos de la clase base. La idea es modelar una relación “es-un” (por ejemplo, Coche es un Vehículo), donde la clase más general aporta el comportamiento común y las clases más específicas añaden o especializan funcionalidades, reduciendo duplicación de código y creando jerarquías más claras y mantenibles.

class Animal:
    """Clase base (superclase)"""
    def __init__(self, nombre):
        self.nombre = nombre
    
    def hacer_sonido(self):
        return "Sonido genérico"

class Perro(Animal):
    """Clase derivada (subclase) hereda de Animal"""
    pass

# Uso
perro = Perro("Max")
print(perro.nombre)
# Output: Max (heredado)
print(perro.hacer_sonido())
# Output: Sonido genérico (heredado)

La herencia se define colocando entre paréntesis el nombre de la clase padre inmediatamente después del nombre de la clase hija en la declaración de la clase.

La subclase automáticamente recibe todos los métodos y atributos de la clase padre, pudiendo sobreescribirlos o extenderlos según sea necesario.

Herencias

La herencia es un mecanismo que permite definir una nueva clase a partir de otra existente, de forma que la clase derivada reutiliza, amplía o modifica los atributos y métodos de la clase base. La idea es modelar una relación “es-un” (por ejemplo, Coche es un Vehículo), donde la clase más general aporta el comportamiento común y las clases más específicas añaden o especializan funcionalidades, reduciendo duplicación de código y creando jerarquías más claras y mantenibles.

class Animal:
    """Clase base (superclase)"""
    def __init__(self, nombre):
        self.nombre = nombre
    
    def hacer_sonido(self):
        return "Sonido genérico"

class Perro(Animal):
    def hacer_sonido(self):
        return "¡Guau! ¡Guau!"

class Gato(Animal):
    def hacer_sonido(self):
        return "Miau miau"

# Uso
perro = Perro("Rex")
gato = Gato("Whiskers")
print(perro.hacer_sonido())
# Output: ¡Guau! ¡Guau!
print(gato.hacer_sonido())
# Output: Miau miau

La herencia se define colocando entre paréntesis el nombre de la clase base inmediatamente después del nombre de la clase derivada en la declaración de la clase.

La subclase automáticamente recibe todos los métodos y atributos de la clase base, pudiendo sobreescribirlos o extenderlos según sea necesario.

Herencias

La herencia es un mecanismo que permite definir una nueva clase a partir de otra existente, de forma que la clase derivada reutiliza, amplía o modifica los atributos y métodos de la clase base. La idea es modelar una relación “es-un” (por ejemplo, Coche es un Vehículo), donde la clase más general aporta el comportamiento común y las clases más específicas añaden o especializan funcionalidades, reduciendo duplicación de código y creando jerarquías más claras y mantenibles.

class Animal:
    """Clase base (superclase)"""
    def __init__(self, nombre):
        self.nombre = nombre
    
    def hacer_sonido(self):
        return "Sonido genérico"

class Perro(Animal):
    def __init__(self, nombre, raza):
         # Llama al constructor de Animal
        Animal.__init__(nombre)
        # super().__init__(nombre)  # Llama constructor padre
        self.raza = raza

    def hacer_sonido(self):
        return "¡Guau! ¡Guau!"

class Gato(Animal):
    def hacer_sonido(self):
        return "Miau miau"

# Uso
perro = Perro("Rex", "Golden Retriever")
gato = Gato("Whiskers")
print(perro.hacer_sonido())
# Output: ¡Guau! ¡Guau!
print(gato.hacer_sonido())
# Output: Miau miau

Usar super() o el nombre de la clase para llamar al constructor de la clase padre:

Además, podemos usar isintance() y issubclass() para evaluar el tipo de la clase y sus herencias:

print(isinstance(perro, Perro))   # Output: True
print(isinstance(perro, Animal))  # Output: True
print(issubclass(Perro, Animal))  # Output: True

Métodos especiales

from typing import Union

class Punto2D:
    """
    Representa un punto en el plano cartesiano 2D.
    """
    
    def __init__(self, x=0, y=0) -> None:
        """Constructor: crea punto con coordenadas x, y"""
        self.x = float(x)
        self.y = float(y)
    
    def __repr__(self) -> str:
        """Representación oficial para debug/reconstrucción"""
        return f'Punto2D({self.x}, {self.y})'
    
    def __str__(self) -> str:
        """Representación legible para usuario final"""
        return f'({self.x}, {self.y})'
    
    def __eq__(self, other) -> bool:
        """Comparación de igualdad"""
        if not isinstance(other, Punto2D):
            return NotImplemented
        return self.x == other.x and self.y == other.y
    
    def __lt__(self, other) -> bool:
        """Menor que: orden lexicográfico (primero x, luego y)"""
        if not isinstance(other, Punto2D):
            return NotImplemented
        return (self.x, self.y) < (other.x, other.y)
    
    def __len__(self) -> int:
        """Número de dimensiones (siempre 2)"""
        return 2
    
    def __bool__(self) -> bool:
        """Verdadero si no es el origen (0,0)"""
        return self.x != 0 or self.y != 0
    
    def __add__(self, other: "Point2D") -> "Point2D":
        """Suma vectorial: p1 + p2"""
        if isinstance(other, Punto2D):
            return Punto2D(self.x + other.x, self.y + other.y)
        return NotImplemented
    
    def __sub__(self, other: "Point2D") -> "Point2D":
        """Resta vectorial: p1 - p2"""
        if isinstance(other, Punto2D):
            return Punto2D(self.x - other.x, self.y - other.y)
        return NotImplemented
    
    def __mul__(self, escalar: Union[int, float]) -> "Point2D":
        """Multiplicación por escalar"""
        if isinstance(escalar, (int, float)):
            return Punto2D(self.x * escalar, self.y * escalar)
        return NotImplemented
    
    def __getitem__(self, indice):
        """Acceso por índice: p[0] = x, p[1] = y"""
        if indice == 0:
            return self.x
        elif indice == 1:
            return self.y
        raise IndexError("Índice fuera de rango (0 o 1)")
    
    def distancia_origen(self):
        """Distancia al origen (0,0)"""
        return (self.x ** 2 + self.y ** 2) ** 0.5

En una clase de Python hay muchos métodos “dunder” (o mágicos), que son convenciones que Python usa para llamar a métodos especiales y atributos

Identidad y ciclo de vida

  • __init__(self, ...): constructor; inicializa el estado del objeto al crearlo.
  • __del__(self) (poco recomendable en la práctica): se ejecuta justo antes de que el objeto sea destruido, normalmente no se usa salvo casos muy específicos.

Representación del objeto

  • __repr__(self): representación “oficial”, pensada para debug y para que, si es posible, sea un string que permita reconstruir el objeto (Point2D(1, 2)). Se usa en repr(x) y en muchas shells interactivas.
  • __str__(self): representación “amigable” para usuarios finales; se usa en str(x) y print(x) cuando está definido, y normalmente es más legible que __repr__.

Operadores lógicos

  • __eq__(self, other): define igualdad (==).
  • __lt__(self, other): “less than”, define < (orden estricto).

Opcionalmente, según necesidad: __le__ (\(\le\)), __gt__ (\(\gt\)), __ge__ (\(\ge\)), __ne__ (\(\ne\)).

Muchas veces se implementa solo un subconjunto y los otros se infieren para completar el resto.

Operaciones básicas útiles

  • __len__(self): permite usar len(obj) (colecciones, contenedores, etc.).
  • __bool__(self): define la verdad lógica del objeto en contextos booleanos (if obj:), si no existe se usa __len__ por defecto.

Operadores matemáticos

Para tipos numéricos o “like-number”:

  • __add__, __sub__, __mul__, etc.: sobrecarga de los operadores suma (\(+\)), resta (\(-\)), multiplicación (\(·\)) cuando tiene sentido para el modelo.
  • Existen más operadores: división __truediv__ (\(/\)), o los “in place” __iadd__ (+=), __isub__ (-=).

Contenedores

Para tipos que representan colecciones:

  • __getitem__(self, key): acceso obj[key] (índices o claves).
  • __setitem__(self, key, value): asignación obj[key] = value.
  • __iter__(self): hace que el objeto sea iterable en for, list(obj), etc.
# Crear puntos
p1 = Punto2D(3, 4)
p2 = Punto2D(3, 4)
p3 = Punto2D(1, 2)

# Representaciones
print(repr(p1))  # Punto2D(3.0, 4.0)
print(str(p1))   # (3.0, 4.0)
print(p1)        # (3.0, 4.0)

# Comparaciones
print(p1 == p2)  # True
print(p1 < p3)   # False

# Operaciones
print(p1 + p3)   # Punto2D(4.0, 6.0)
print(p1 * 2)    # Punto2D(6.0, 8.0)

# Colección-like
print(len(p1))       # 2
print(p1[0])         # 3.0
print(bool(p1))      # True
print(bool(Punto2D()))  # False

print(p1.distancia_origen())  # 5.0

Librerias externas

Las librerías externas amplían las capacidades del lenguaje para dominios especializados como análisis de datos, calculo matricial y modelado financiero. Requieren instalación previa con pip (gestor de paquetes):

python -m pip install numpy       # Computación numérica
python -m pip install pandas      # Análisis de datos
python -m pip install scipy       # Colección de funciones matemáticas
python -m pip install matplotlib  # Visualización
python -m pip install QuantLib    # Finanzas cuantitativas

NumPy proporciona arrays multidimensionales y operaciones matemáticas vectorizadas para cálculos numéricos de mayor rendimiento, evitando los loops de Python que son maslentos. Soporta álgebra lineal, estadística descriptiva y generación de números aleatorios, siendo la base de casi todas las librerías científicas en Python.

Su estructura fundamental no es una lista de Python, sino el ndarray (N-dimensional array). A diferencia de las listas tradicionales que son contenedores flexibles de punteros a objetos dispersos en memoria, un ndarray es un bloque de memoria contiguo que contiene elementos de un solo tipo de dato (homogéneos):

  • Eficiencia en Memoria: Al forzar que todos los datos sean del mismo tipo
  • Multidimensionalidad: Los arrays tienen “ejes” (axes) y una “forma” (shape). Un array puede representar desde un simple vector (1D) o una matriz (2D), hasta tensores de orden superior (3D, 4D…)
  • Vectorización, aumentando la velocidad de ejecución, las operaciones vectorizadas permiten aplicar funciones matemáticas a arrays enteros de una sola vez.

Informacion: https://numpy.org/doc/2.4/

Pandas ofrece estructuras de datos como DataFrames (tablas) y Series (vectores), con herramientas para lectura/escritura de ficheros (CSV, Excel, SQL), limpieza de datos, transformaciones, agrupaciones y análisis exploratorio.

Es la biblioteca de referencia en el ecosistema de Python para la manipulación y análisis de datos estructurados.

El núcleo de Pandas se basa en dos estructuras de datos principales: las Series (unidimensionales, como una lista o una columna) y los DataFrames (bidimensionales, como una tabla).

Informacion: https://pandas.pydata.org/docs/

SciPy es una librería de cómputo científico construida sobre NumPy, que añade módulos de alto nivel para optimización, integración numérica, estadística avanzada, procesamiento de señales, álgebra lineal y más. Es especialmente útil en contextos de ingeniería y ciencia aplicada, donde se necesitan algoritmos numéricos robustos como métodos de optimización, resolución de ecuaciones diferenciales o ajustes de modelos.

Informacion: https://docs.scipy.org/doc/scipy/

Matplotlib genera gráficos (líneas, barras, scatter, histogramas)** con control sobre estilos, colores, etiquetas y anotaciones, permite crear visualizaciones estáticas interactivas para reportes, presentaciones y análisis exploratorio de datos.

Es la librería más antigua para la visualización de datos en Python. Fue diseñada originalmente para imitar el comportamiento de graficado de MATLAB.

Información: https://matplotlib.org/

QuantLib es una librería especializada en valoración de derivados, calibración de curvas de tipos de interés, cálculo de volatilidad, análisis de riesgos y simulación de Monte Carlo.​

Informacion: https://quantlib-python-docs.readthedocs.io/en/latest/

Numpy (1/3)


import numpy as np

# Array 1D (vector)
array_1d = np.array([1, 2, 3, 4, 5])
# Array 2D (matriz)
array_2d = np.array([[1, 2, 3], 
                     [4, 5, 6]])
# Array 3D
array_3d = np.array([[[1, 2], [3, 4]], 
                     [[5, 6], [7, 8]]])
# Array de ceros
zeros = np.zeros((3, 4))  # 3 filas, 4 columnas
# Array de unos
ones = np.ones((2, 3))
# Array con un valor específico
full = np.full((2, 2), 7)  # Matriz 2x2 llena de 7s
# Array identidad (matriz diagonal con 1s)
identity = np.eye(3)

array_3d.shape             # Dimensiones
array_3d.dtype             # Tipo de dato
array_3d.size              # Total de elementos

# Indexación y slicing
array_1d[0]                # Primer elemento
array_1d[-1]               # Ultimo elemento
array_1d[1:4]              # Elementos 1 a 3
array_1d[::2]              # Cada 2 elementos
array_2d[0, 1]             # Fila 0, columna 1

array_2d.reshape(3, 2)     # Redimensionar
array_2d.T                 # Transpuesta
  1. Array N-dimensional (ndarray) de python. Al contrario que de las listas son homogéneos (mismo tipo), lo que permite un almacenamiento en memoria mucho más eficiente y operaciones matemáticas mas rapidas.
  • Array 1D (Vector): [1, 2, 3…] similar a una lista plana.
  • Array 2D (Matriz): Lista de listas o matrix.
  • Array 3D (Tensor): “Cubo” de datos.
  1. Creación eficiente de arrays: np.zeros, np.ones(), np.full() y np.eye()

  2. Metadatos: .shape, .dtype y .size

  3. Indexación y slicing

  4. Manipulación de la estructura .T y .reshape()

Numpy (2/3)


  1. Operaciones aritméticas Vectorizadas (Element-wise), NumPy aplica las operación a todo el bloque de datos simultáneamente.

  2. Filtros (Boolean Masking) para filtrar datos sin usar condicionales if.

  3. Manipulación de Formas (Concatenate & Stack) unir arrays, esencial

    • Concatenate: Une vectores uno detrás de otro (crece en longitud).

    • Stacking (Apilado):

      • vstack (Vertical): Añade filas.
      • hstack (Horizontal): Añade columnas.
import numpy as np

a = np.array([1, 2, 3, 4])
b = np.array([5, 6, 7, 8])

a + b       # Suma
a - b       # Resta
a * b       # Multiplicación
b / a       # División
a ** 2      # Potencia
np.sqrt(a)  # Raíz cuadrada

array = np.array([10, 20, 30, 40, 50, 60])

mask = array > 30       # elementos que cumplen la condición)
resultado = array[mask] # Aplicar la máscara para obtener elementos
array[array > 30]       # [40 50 60]
array[(array > 20) & (array < 50)] # Múltiples condiciones

a = np.array([1, 2, 3])
b = np.array([4, 5, 6])

concat = np.concatenate([a, b]) # Concatenar (juntar end-to-end)
concat  # [1 2 3 4 5 6]

# Stack (apilar verticalmente)
m1 = np.array([[1, 2], [3, 4]])
m2 = np.array([[5, 6], [7, 8]])

vstacked = np.vstack([m1, m2])  # Apilar filas
hstacked = np.hstack([m1, m2])  # Apilar columnas

Numpy (3/3)


import numpy as np

data = np.array([10, 20, 30, 40, 50])

np.mean(data)   # Media (promedio)
np.median(data) # Mediana
np.std(data)    # Desviación estándar
np.var(data)    # Varianza
np.min(data)    # Mínimo y máximo
np.max(data)
np.sum(data)    # Suma y producto
np.prod(data)
np.percentile(data, 25) # Percentil

a = np.array([[1, 2], [3, 4]])
b = np.array([[5, 6], [7, 8]])

producto = np.dot(a, b)  # O: a @ b
transpuesta = a.T        # Transpuesta
det = np.linalg.det(a)   # Determinante

inversa = np.linalg.inv(a) # Matriz inversa

# Numeros pseudo aleatorios
# Establecer semilla
np.random.seed(42)
# Uniformes [0, 1)
random_uniform = np.random.rand(5)
# Normales estandar(distribución Gaussiana)
random_normal = np.random.randn(5)
# Enteros aleatorios en rango [0, 10)
random_int = np.random.randint(0, 10, 5)
# Array aleatorio 3x3
random_matrix = np.random.rand(3, 3)
  1. Estadística básica en arrays: np.mean, np.median, np.std, np.var, np.min, np.max, np.sum, np.prod y np.percentile.

  2. Álgebra lineal: producto matricial np.dot(a, b), transpuesta a.T, determinante np.linalg.det(a)e inversa np.linalg.inv(a).

  3. Numeros pseudoaleatorios: fijar semillas np.random.seed(42), simulación de números aleatorios uniformes np.random.rand(5), normales estándar con np.random.randn(5), enteros con np.random.randint(0, 10, 5) y una matriz aleatoria 3x3 con np.random.rand(3, 3)

Pandas (1/5)

import pandas as pd

serie1 = pd.Series([10, 20, 30, 40])
serie2 = pd.Series([10, 20, 30, 40], 
                   index=['A', 'B', 'C', 'D'])

serie2['B']  # 20 - Acceso por etiqueta
serie2['A']  # 10
# Crear serie desde diccionario
datos = {'España': 47, 'Francia': 67, 'Alemania': 83, 'Italia': 59}
serie_paises = pd.Series(datos)

# Atributos de pd.Serie
serie_paises.size
serie_paises.index
serie_paises.dtype
serie_paises.values

# Dataframe
# Desde diccionario donde cada clave es una columna
datos = {
    'Nombre': ['Alice', 'Bob', 'Carlos', 'Diana'],
    'Edad': [25, 30, 28, 35],
    'Salario': [50000, 60000, 55000, 70000],
    'Departamento': ['Ventas', 'IT', 'Finanzas', 'IT']
}

# Desde lista de datos
datos = [
    ['Alice', 25, 50000],
    ['Bob', 30, 60000],
    ['Carlos', 28, 55000]
]

df = pd.DataFrame(datos, 
                  columns=['Nombre', 'Edad', 'Salario'])
df.shape
df.columns      # Nombre de columnas
df.index        # RangeIndex(start=0, stop=3, step=1)
df.dtypes
df.info()       # Tipo de dato, valores no-nulos
df.head()       # Por defecto, primeras 5 filas
df.head(2)      # Primeras 2 filas
df.tail()       # Últimas 5 filas
  1. La Serie (pd.Series): Un Array con Etiquetas. A diferencia de un array de NumPy (que solo tiene posiciones numéricas 0, 1, 2…), la Serie permite definir un índice explícito (etiquetas como ‘A’, ‘B’, ‘España’).

  2. El DataFrame (pd.DataFrame): Es una colección de Series alineadas (comparten el mismo índice), formando una tabla de filas y columnas. Se construye desde:

  • Diccionario: Clave = Nombre de Columna, Valor = Lista de datos.
  • Desde Lista de Listas.
  1. Exploración rápida del dataframe:
  • .head() / .tail(): Muestra las primeras/últimas filas.
  • .info(): Muestra tipos de datos, uso de memoria y conteo de nulos.
  • .shape y .columns: Dimensiones (filas, columnas) y nombres de variables.

Pandas (2/5)

  1. Selección de Datos: .loc vs .iloc
  • .loc (Label-based): Busca por etiqueta/nombre. df.loc[0, 'Nombre'] significa “Fila con índice 0 y Columna llamada ‘Nombre’”.
  • .iloc (Integer-based): Busca por posición numérica (como NumPy). df.iloc[0] es “La primera fila”, independientemente de cómo se llame su índice.
  1. Filtrado Condicional (Queries): extraer subconjuntos de datos. Condiciones Lógicas: df[df['Edad'] > 28] devuelve solo las filas que cumplen la condición. Operadores Combinados: Uso de & (AND) y | (OR) para lógica compleja. Filtros de Texto: Métodos vectorizados de strings como .str.contains('li') o .isin([...]).

  2. Transformación y Edición: nuevas columnas 2026 - df['Edad'] o un valor constante, .drop(..., axis=1): Elimina columnas. .rename(): renombra variables para mayor claridad. .sort_values(): Ordena los datos soporta múltiples niveles de ordenación.

df = pd.DataFrame({
    'Nombre': ['Alice', 'Bob', 'Carlos'],
    'Edad': [25, 30, 28],
    'Salario': [50000, 60000, 55000]
})

df.Nombre # Columnas
df['Edad']
df[['Nombre', 'Salario']] # Múltiples columnas

df.loc[0] # Filas
df.loc[0:2]

df.loc[0, 'Nombre'] # Elemento

df.iloc[0] # Por posición (.iloc[])
df.iloc[0:2]
df.iloc[-2:]

# Filtros
df[df['Edad'] > 28]
df[(df['Edad'] > 27) & (df['Salario'] > 55000)]
df[df['Nombre'].isin(['Alice', 'Bob'])]
df[df['Nombre'].str.contains('li')]

# Crear nuevas columnas
df['Fecha Nacimiento'] = 2026 - df['Edad']
df['País'] = 'España'

# Eliminar una columna
df = df.drop('País', axis=1)
df = df.rename(columns={'Edad': 'Años', 'Salario': 'Sueldo'})
df.sort_values(by='Edad')
df.sort_values(by=['Edad', 'Salario'])
df.sort_index()

Pandas (3/5)

df = pd.DataFrame({
    'Nombre': ['Alice', 'Bob', np.nan, 'Diana'],
    'Edad': [25, np.nan, 28, 35],
    'Salario': [50000, 60000, 55000, np.nan]
})

# Detectar valores faltantes
print(df.isna())        # Matriz booleana
print(df.isnull())      # Alias para isna()

# Contar valores faltantes
print(df.isna().sum())  # Por columna
# Output:
# Nombre      1
# Edad        1
# Salario     1

# Eliminar filas con valores faltantes
df_limpio = df.dropna()
print(df_limpio)

# Rellenar valores faltantes
df_relleno = df.fillna(0)           # Rellenar con 0
df_relleno = df.fillna(df.mean())   # Rellenar con la media

# Rellenar hacia adelante (forward fill)
df_ffill = df.fillna(method='ffill')

# Rellenar hacia atrás (backward fill)
df_bfill = df.fillna(method='bfill')

df = pd.DataFrame({
    'Edad': [25, 30, 28, 35, 32],
    'Salario': [50000, 60000, 55000, 70000, 62000]
})

# Estadísticas para una columna
print(df['Edad'].mean())       # Media: 30.0
print(df['Edad'].median())     # Mediana: 30
print(df['Edad'].std())        # Desviación estándar
print(df['Edad'].var())        # Varianza
print(df['Edad'].min())        # Mínimo: 25
print(df['Edad'].max())        # Máximo: 35

# Resumen estadístico completo
print(df.describe())

# Contar valores únicos
print(df['Edad'].nunique())    # Número de valores únicos

# Obtener valores únicos
print(df['Edad'].unique())     # [25 30 28 35 32]

# Contar ocurrencias
print(df['Edad'].value_counts())
  1. Gestión de Datos Faltantes: df.isna().sum() ver rápidamente la calidad de tus datos. dropna borrar filas incompletas. fillna: rellenar los huecos para no perder información.

  2. Estadística Descriptiva: Cálculo directo de mean (promedio), median (mediana), std (volatilidad) ignorando automáticamente los nulos. describe(): información estadistica del DataFrame (cuartiles, extremos, media).

  3. Análisis de Frecuencias: value_counts() y nunique() ver qué valores son los más comunes o repetidos.

Pandas (4/5)

  1. Agregación (groupby): Creación de grupos, para aplicar métricas .agg(['mean', 'min', 'max']) calcula varios estadisticas para cada grupo,

  2. Concatenación (concat): unir pd.DataFrames

  • Vertical (Eje 0)
  • Horizontal (Eje 1): Añadir nuevas columnas a un dataset existente, asumiendo que el orden de las filas coincide.
  1. Unión (merge): cruzar información de fuentes distintas usando una clave común (ID), replicando los JOIN de SQL.
  • Inner Join: Muestra solo lo que coincide en ambas tablas (intersección).
  • Left/Right/Outer Join: Dice qué hacer con los datos que no cruzan (preservar todo lo de la izquierda, todo lo de la derecha, o ambos).
df = pd.DataFrame({
    'Departamento': ['Ventas', 'IT', 'Finanzas', 'Ventas', 'IT', 'Finanzas'],
    'Empleado': ['Alice', 'Bob', 'Carlos', 'Diana', 'Eve', 'Frank'],
    'Salario': [50000, 60000, 55000, 52000, 65000, 58000]
})

# Agrupar por departamento
grupo = df.groupby('Departamento')

# Calcular la media salarial por departamento
print(grupo['Salario'].mean())

print(grupo['Salario'].agg(['mean', 'min', 'max', 'count']))

print(grupo[['Salario']].describe())

print(grupo.size())

df1 = pd.DataFrame({
    'Nombre': ['Alice', 'Bob'],
    'Edad': [25, 30]
})

df2 = pd.DataFrame({
    'Nombre': ['Carlos', 'Diana'],
    'Edad': [28, 35]
})

# Concatenar verticalmente (apilar filas)
df_concat = pd.concat([df1, df2], ignore_index=True)

# Concatenar horizontalmente (apilar columnas)
df3 = pd.DataFrame({
    'Salario': [50000, 60000, 55000, 70000]
})

df_concat_h = pd.concat([df_concat, df3], axis=1)

# Tabla de empleados
empleados = pd.DataFrame({
    'ID': [1, 2, 3],
    'Nombre': ['Alice', 'Bob', 'Carlos'],
    'Departamento': ['Ventas', 'IT', 'Finanzas']
})

# Tabla de salarios
salarios = pd.DataFrame({
    'ID': [1, 2, 3],
    'Salario': [50000, 60000, 55000]
})

# Merge interno (INNER JOIN)
df_merged = pd.merge(empleados, salarios, on='ID')
print(df_merged)

# Merge izquierdo (LEFT JOIN)
df_left = pd.merge(empleados, salarios, on='ID', how='left')

# Merge derecho (RIGHT JOIN)
df_right = pd.merge(empleados, salarios, on='ID', how='right')

# Merge externo (OUTER JOIN)
df_outer = pd.merge(empleados, salarios, on='ID', how='outer')

Pandas (5/5)

import pandas as pd

# Leer un CSV
df = pd.read_csv('datos.csv')

# Guardar a CSV
df.to_csv('datos_procesados.csv', index=False)

# Parámetros útiles
df = pd.read_csv('datos.csv', 
                  sep=';',           # Separador (por defecto ',')
                  encoding='utf-8',  # Codificación
                  nrows=1000)        # Leer solo primeras 1000 filas

# Leer Excel
df = pd.read_excel('datos.xlsx', sheet_name=0)

# Guardar a Excel
df.to_excel('datos.xlsx', index=False)

# Escribir múltiples hojas
with pd.ExcelWriter('datos.xlsx') as writer:
    df1.to_excel(writer, sheet_name='Hoja1', index=False)
    df2.to_excel(writer, sheet_name='Hoja2', index=False)

# Leer JSON
df = pd.read_json('datos.json')

# Guardar a JSON
df.to_json('datos.json', orient='records')

df = pd.DataFrame({
    'Nombre': ['Alice', 'Bob', 'Carlos'],
    'Salario': [50000, 60000, 55000]
})

# Función personalizada
def categorizar_salario(salario):
    if salario < 52000:
        return 'Bajo'
    elif salario < 58000:
        return 'Medio'
    else:
        return 'Alto'

# Aplicar a una columna
df['Categoria'] = df['Salario'].apply(categorizar_salario)

# Usar lambda (funciones anónimas)
df['Salario_Ajustado'] = df['Salario'].apply(lambda x: x * 1.1)  # Aumentar 10%

# Aplicar a toda la fila
df['Nombre_Largo'] = df.apply(lambda row: row['Nombre'] + ' (' + str(row['Salario']) + ')', axis=1)

df = pd.DataFrame({
    'Mes': ['Enero', 'Enero', 'Febrero', 'Febrero'],
    'Región': ['Norte', 'Sur', 'Norte', 'Sur'],
    'Ventas': [1000, 1500, 1200, 1800]
})

# Tabla dinámica
pivot = df.pivot_table(values='Ventas', 
                       index='Mes',
                       columns='Región',
                       aggfunc='sum')
  1. Input/Output Pandas es capaz de leer y escribir en los formatos más comunes de la industria: archivos de texto plano (CSV, JSON) hasta hojas de cálculo (Excel). Permite ajustar la lectura al archivo: definir separadores (sep=';'), o cargar solo una muestra (nrows=1000) para archivos grances.

  2. Aplicación de funciones (apply) si las funciones predefinidas no son suficientes, apply permte aplicar funciones definidas por el usuario. Permite operar celda a celda o fila a fila (axis=1).

  3. Pivot Tables tablas finámicas como en excel

QuantLib

Agenda:

  1. Introducción a la programación en Python

  2. Python

  3. QuantLib

  4. Valoración de derivados financieros

  5. Modelo multifactorial de riesgo de crédito

Fechas / Contadores / Calendarios


import QuantLib as ql

today = ql.Date(15, 6, 2020)  # Día, Mes, Año
# Establecer como fecha de evaluación global
ql.Settings.instance().evaluationDate = today
print(ql.Settings.instance().evaluationDate)  # June 15th, 2020

# Sumar días
future_date = today + 30  # 30 días después

# Sumar períodos
six_months = today + ql.Period(6, ql.Months)
six_months = today + ql.Period("6M")

# Diferencia entre fechas
days_diff = (six_months - today)

# Calendario: festivos de Estados Unidos
calendar = ql.UnitedStates(ql.UnitedStates.NYSE)

# Contador de días: Actual/360 (mercados monetarios)
dayCounter = ql.Actual360()

# Contador de días: Actual/365 (bonos, derivados)
dayCounter_365 = ql.Actual365Fixed()

# Contador de días: 30/360 (mercados corporativos)
dayCounter_30_360 = ql.Thirty360()

# Contador de días: 30/360 (mercados corporativos)
dayCounter_bus_252 = ql.Business252(calendar)

# Calcular fracción de año
start_date = ql.Date(1, 1, 2020)
end_date = ql.Date(30, 6, 2020)
(end_date - start_date) / 360
year_fraction = dayCounter.yearFraction(start_date, end_date)

calendar.businessDaysBetween(start_date, end_date) / 252
dayCounter_bus_252.yearFraction(start_date, end_date)
  1. ql.Settings.instance().evaluationDate - fecha de referencia para descontar los flujos de caja futuros.

  2. Fechas y operaciones​ con ql.Period()

  3. Calendarios de Mercado los mercados no abren todos los días. Estos calendarios implemtan los festivos históricos y futuros de los principales meracados, como la bolsa de Nueva York, Londres, o TARGET en Europa.

  4. Convenciones de Conteo de Días (DayCounters) Define cómo se mide un año en diferentes mercados, lo que determina el cálculo de intereses (devengo):

  • Actual/360: Estándar en mercados monetarios.
  • Actual/365: Usado en bonos gubernamentales y derivados.
  • 30/360: Estándar en bonos corporativos de EE.UU. (asume todos los meses de 30 días para simplificar pagos).
  • Business/252: Usado en mercados emergentes como Brasil, donde solo cuentan los días hábiles (~252 al año).

¿Que día es festivo en EEUU segun el calendario de la Bolsa de Nueva York?

  • 5 de Febrero de 2024
  • 4 de Junio de 2024
  • 15 de Agosto de 2024
  • 26 de Noviembre de 2024

¿Cuántos dia abrió la bolsa de Nueva York en 2025?

  • 250
  • 251
  • 252
  • 253
  • 254

Schedules (1/2)

ql.MakeSchedule en QuantLib es una función que simplifica la creación de objetos ql.Schedule (objeto para generar y gestionar la lista de fechas relevantes)

import QuantLib as ql
import pandas as pd

start_date = ql.Date(28, 2, 2021)
end_date   = ql.Date(31, 3, 2025)
tenor      = ql.Period("6M")

# Parámetros obligatorios
schedule = ql.MakeSchedule(
    effectiveDate    = start_date,
    terminationDate  = end_date,
    tenor            = tenor)

# Parámetros opcionales
calendar_schedule = ql.MakeSchedule(
    effectiveDate   = start_date,
    terminationDate = end_date,
    tenor           = tenor, 
    calendar        = ql.TARGET())

pd.DataFrame({"sin_calendario": list(schedule),
              "con_calendario": list(calendar_schedule)})

# business day convention
# [ql.Following, ql.ModifiedFollowing, ql.Preceding, ql.ModifiedPreceding]
bus_con_schedule = ql.MakeSchedule(
    effectiveDate   = start_date,
    terminationDate = end_date,
    tenor           = tenor, 
    calendar        = ql.TARGET(),
    convention      = ql.ModifiedFollowing)

pd.DataFrame({"sin_calendario": list(schedule),
              "con_calendario": list(calendar_schedule)},
              "con_bus_conven": list(calendar_schedule))

Parámetros Obligatorios

  • effectiveDate (ql.Date): punto de partida para generar las fechas.
  • terminationDate (ql.Date): Fecha de finalización.
  • tenor o frequency (ql.Period o ql.Frequency): determina cada cuánto tiempo se genera una fecha ql.Period('6M') o ql.Semiannual.

Parámetros Opcionales de ajuste

  • calendar (ql.Calendar): calendario de festivos
  • convention (ql.BusinessDayConvention): ajuste de fechas intermedias si caen en festivo.
  • terminationDateConvention (ql.BusinessDayConvention): Igual que convention, pero específico para la fecha de vencimiento. El vencimiento puede tener reglas diferentes a los cupones intermedios.

Schedules (2/2)

Su función principal es generar la lista de fechas de pago de un instrumento financiero (ej. cupones de un bono), aplicando automáticamente la lógica de calendarios y festivos.


Parámetros de Generación de Fechas (Rules)

Controlan la de generación antes de ajustar por festivos.

  • rule (ql.DateGeneration.Rule): Backward (Default): desde la fecha de vencimiento hacia atrás, Forward: desde la fecha de inicio hacia adelante, Zero, ThirdWednesday.
  • endOfMonth (bool): si la fecha de inicio es fin de mes, todas las fechas subsiguientes sean también fin de mes.

Parámetros para Periodos Irregulares

  • firstDate (ql.Date): fuerza una fecha específica para la primera fecha/cupón.
  • nextToLastDate (ql.Date): fuerza una fecha específica para el penúltimo cupón.
calendar   = ql.TARGET()
convention = ql.ModifiedFollowing

schedule_backward = ql.MakeSchedule(
    effectiveDate   = start_date,
    terminationDate = end_date,
    tenor           = tenor,
    calendar        = calendar,
    convention      = convention,
    rule            = ql.DateGeneration.Backward)

schedule_forward = ql.MakeSchedule(
    effectiveDate   = start_date,
    terminationDate = end_date,
    tenor           = tenor,
    calendar        = calendar,
    convention      = convention,
    rule            = ql.DateGeneration.Forward)

schedule_eom = ql.MakeSchedule(
    effectiveDate   = start_date,
    terminationDate = end_date,
    tenor           = ql.Period("1M"),
    calendar        = calendar,
    convention      = convention,
    endOfMonth      = True)

schedule_first_long = ql.MakeSchedule(
    effectiveDate   = start_date,
    terminationDate = end_date,
    tenor           = tenor,
    calendar        = calendar,
    convention      = convention,
    rule            = ql.DateGeneration.Backward,
    firstDate       = ql.Date(15, 3, 2021)

Quiz MakeSchedule

Usando ql.MakeSchedule() y calendario ql.TARGET. En el calendario aparece una fecha que cae en sábado 31/05/2025. Si QuantLib ajusta las fechas con la convención ql.ModifiedFollowing, ¿a qué fecha laborable se moverá?

  • Lunes 02/06/2025
  • Viernes 30/05/2025
  • Sábado 31/05/2025 (no se ajusta)
  • Jueves 29/05/2025

Curvas de tipos

Una curva de tipos de interés (yield curve) es una función que muestra la relación entre el plazo de vencimiento y la tasa de interés de descuento. Permite:

  • Valorar cualquier instrumento de renta fija (bonos, swaps, derivados)
  • Derivar los forward implícitos (expectativas del mercado sobre tasas futuras)

Las curvas de tipos normalmente se obtienen a través de:

Bootstrapping: Construye una curva que reprecia exactamente los instrumentos de mercado. Usa interpolación local (lineal, cúbica). Ver (Ametrano & Bianchetti, 2013).

Fitting: Ajusta parámetros de un modelo paramétrico (Nelson-Siegel, Svensson) minimizando diferencias de precios. Produce curvas más suaves pero aproximadas.

import QuantLib as ql

today = ql.Date(15, 6, 2020)
ql.Settings.instance().evaluationDate = today

# Parámetros
settlement_days = 2
calendar = ql.UnitedStates(ql.UnitedStates.NYSE)
rate = 0.05
day_counter = ql.Actual360()

# Crear curva plana
flat_forward = ql.FlatForward(settlement_days, calendar,
                              rate, day_counter)

flat_forward.referenceDate()
flat_forward.discount(today + ql.Period(1, ql.Years))

# Definir puntos de la curva
dates = [ql.Date(15, 6, 2020), 
         ql.Date(15, 12, 2020), 
         ql.Date(15, 6, 2021), 
         ql.Date(15, 6, 2022)]

yields = [0.01, 0.015, 0.02, 0.025]  # Tasas en cada fecha

day_counter = ql.Actual360()

# Crear curva con interpolación lineal
curve = ql.LogLinearZeroCurve(dates, yields, day_counter)

# Handle para usar la curva
curve_handle = ql.YieldTermStructureHandle(curve)

# Obtener tasa forward a una fecha
target_date = ql.Date(15, 9, 2020)
forward_rate = curve_handle.forwardRate(target_date, target_date,
                                        day_counter, ql.Simple)
forward_rate.rate()

Valoración de instrumentos

La libreria QuantLib valora los instrumentos financieros a través de dos piezas:

  • Instrumento (ql.Instrument): qué contrato tengo (bono, swap, opción, …) y que características contractuales tengo (fechas, cupones, strike, …).
  • Pricing engine (ql.PricingEngine): con qué modelo y con qué datos de mercado lo valoro.

Y se conectan a través de .setPricingEngine(engine):

instrumento = ...
engine = ...
instrumento.setPricingEngine(engine)
valor = instrument.NPV()


Instrumento

Un instrumento es un objeto que representa un producto financiero concreto. Los parámetros fijos del contrato:

  • Bonos: nominal, cupón, fechas de pago…
  • Swaps: notional, tipo fijo, índice flotante, calendarios…
  • Opciones: tipo (call/put), strike, fecha de vencimiento…

Pricing engine

Un pricing engine es un objeto que define:

  • Qué modelo usar, dependiendo del ql.Instrument (descuento de flujos, Black‑Scholes, árbol binomial, …)
  • Qué datos de mercado usar (curvas, volatilidades, spot…)

El Bono

Un bono es esencialmente un préstamo troceado en pequeñas partes que cotizan en un mercado. Cuando un inversor compra un bono, está prestando dinero a una entidad (Gobierno o empresa) a cambio de recibir unos intereses periódicos y la devolución del dinero en el futuro.

Aunque se denomine “renta fija”, su precio no es fijo, cambia constantemente en el mercado; lo que es “fijo” son las condiciones del contrato (fechas de pago y fórmula del cupón).

El precio de un bono no es más que la suma de todo el dinero que van a pagar en el futuro, traído a valor presente (descontado) al día de hoy.

\[ P = \sum_{t=1}^n \frac{C}{(1 + r)^t} + \frac{N}{(1 + r)^t} \]

Donde, \(P\): Precio actual del bono, \(C\): Cupón, \(N\): Nominal, \(r\): Tasa de interés de mercado, \(n\): Número de periodos hasta el vencimiento, \(t\): Periodo de tiempo específico de cada flujo


  1. El Entorno (Settings): ql.Settings.instance().evaluationDate = today. Especificar la fecha de valoración

  2. El Instrumento (ql.FixedRateBond). El bono no sabe nada de tipos de interés, solo sabe que cupones tiene comprometidos.

  • settlement_days: Días que tardamos en liquidar (normalmente 2 en Europa/USA).
  • day_count: Convención para contar días (30/360, Actual/360, etc.). Importante para calcular el cupón corrido.
  • FixedRateBond: Es la clase para bonos estándar. Si fuera flotante, usaríamos FloatingRateBond.


  1. Datos de mercado (ql.YieldTermStructureHandle). Valor de los datos de mercado hoy.
  • La Curva: Usamos ql.FlatForward por simplicidad.
  • El Handle: Esto permite que si cambiamos la tasa de la curva más tarde, el bono se actualice automáticamente
  1. Price engine (ql.DiscountingBondEngine). Como valorar, descontando los flujos de caja que genera el bono usando la curva que le hemos dado en el Handle.
  • bond.setPricingEngine(engine): definimos el engine en el instrumento.


  1. Resultados:
  • NPV (Net Present Value): Es el valor total del bono hoy. Normalmente coincide con el Dirty Price.
  • Dirty Price (Precio Sucio): Precio que realmente pagas. Incluye el principal + el cupón corrido (intereses generados desde el último cupón hasta hoy).
  • Clean Price (Precio Limpio): El precio que ves en las pantallas de Bloomberg/Reuters. Es el Dirty Price - Cupon corrido.
  • Yield (TIR): QuantLib tiene un método inverso (bondYield). Le das el precio sucio y te devuelve la TIR.


Ejemplos:

¿Por qué emitir bonos?

¿Qué otras formas alternativas hay de obtener financiación?

¿Qué son las cédulas hipotecarias?

¿Por que los bancos emiten cédulas?

import QuantLib as ql

# --- PASO 1: Configurar el entorno ---
today = ql.Date(15, 2, 2025)
ql.Settings.instance().evaluationDate = today

# --- PASO 2: Definir el Contrato ---
# 2.1 Crear el calendario de pagos (Schedule)
schedule = ql.MakeSchedule(effectiveDate=ql.Date(15, 2, 2024),
    terminationDate=ql.Date(15, 2, 2031), tenor=ql.Period(ql.Annual),
    calendar=ql.TARGET(), convention=ql.Following,
    terminationDateConvention=ql.Following, rule=ql.DateGeneration.Backward,
    endOfMonth=False)

# 2.2 Crear el Bono
coupons = [0.03]  # 3% cupón anual
day_count = ql.ActualActual(ql.ActualActual.ISMA)

fixed_rate_bond = ql.FixedRateBond(2,
                                   100,
                                   schedule,
                                   coupons,
                                   day_count)

# --- PASO 3: Definir datos de mercado (La Curva) ---
rate = 0.04
curve_day_count = ql.Actual360()
curve_handle = ql.YieldTermStructureHandle(ql.FlatForward(today, rate, curve_day_count))

# --- PASO 4: Pricing Engine ---
bond_engine = ql.DiscountingBondEngine(curve_handle)

# Definir engine en el bono
fixed_rate_bond.setPricingEngine(bond_engine)

# --- RESULTADOS ---
print(f"NPV (Valor Presente): {fixed_rate_bond.NPV():.4f}")
print(f"Clean Price: {fixed_rate_bond.cleanPrice():.4f}")
print(f"Dirty Price:  {fixed_rate_bond.dirtyPrice():.4f}")
print(f"Accrued Amount (Cupón corrido): {fixed_rate_bond.accruedAmount():.4f}")

# Cálculo de la TIR dado un precio de mercado
market_price = 95.0
yield_rate = fixed_rate_bond.bondYield(
    market_price,
    day_count,
    ql.Compounded,
    ql.Annual)

print(f"Yield (TIR) para precio {market_price}: {yield_rate:.2%}")

Ejercicio: Valoración de un bono

Estamos a 17 de febrero de 2026 y tenemos que valorar la Obligación a 15 años que el Tesoro español acaba de emitir en enero, BOE. Suponiendo que los tipos interes son de 3% y la estructura temporal es plana.

Calcular con QuantLib:

  • El Precio Sucio (Dirty Price).
  • El Precio Limpio (Clean Price).
  • El Cupón Corrido (Accrued Interest).

Datos del Bono según el BOE:

  • Tipo: Obligación del Estado a 15 años.
  • Vencimiento: 31 de enero de 2041.
  • Cupón: 3.50% anual.
  • Pago de cupón: 31 de enero de cada año.
  • Convención: Actual/Actual (ICMA).
  • Fecha de liquidación: T+2 (Estándar mercado español).

Ejercicio: Valoración de un bono

import QuantLib as ql

valuation_date = ql.Date(17, 2, 2026)
ql.Settings.instance().evaluationDate = valuation_date

#  Definición del Instrumento (Datos del BOE)
issue_date = ql.Date(31, 1, 2026) # Asumimos fecha de origen teórica coincidente con cupón
maturity_date = ql.Date(31, 1, 2041)
coupon_rate = 0.035 # 3.50%
nominal = 100.0

# Calendario y Convenciones (Estándar España)
calendar = ql.TARGET()
day_count = ql.ActualActual(ql.ActualActual.ISMA)
coupon_frequency = ql.Period(ql.Annual)
settlement_days = 2 # T+2

# Construcción del Calendario de Pagos (Schedule)
schedule = ql.MakeSchedule(
    effectiveDate=issue_date,
    terminationDate=maturity_date,
    tenor=coupon_frequency,
    calendar=calendar,
    convention=ql.ModifiedFollowing,
    rule=ql.DateGeneration.Backward,
    endOfMonth=False
)

# Construcción del Objeto Bono
bond = ql.FixedRateBond(
    settlement_days,
    nominal,
    schedule,
    [coupon_rate],
    day_count
)

# 3. Creación de la Curva
# Estructura Temporal Plana (FlatForward)
ts_curve = ql.FlatForward(
    valuation_date, 
    0.03, 
    ql.Actual365Fixed(),
    ql.Compounded,
    ql.Annual)

ts_handle = ql.YieldTermStructureHandle(ts_curve)

# 4. Motor de Descuento (Pricing Engine)
bond_engine = ql.DiscountingBondEngine(ts_handle)
bond.setPricingEngine(bond_engine)

# 5. Resultados
dirty_price = bond.dirtyPrice()
clean_price = bond.cleanPrice()
accrued = bond.accruedAmount()

Precios del bono

El Swap


Un Swap no es más que intercambiar una serie de pagos normalmente unos fijos por otros variables. Para valorarlo, hay que hacer dos cosas:

  • Proyectar: Ver cuánto estima el mercado que valdrá el subyacente en el futuro (usando la curva para calcular los forward).
  • Descontar: Trae esos flujos futuros a valor presente (usando la curva de descuento).

El valor de un Swap (\(V_{swap}\)) para quien paga fijo y recibe flotante (Payer) es la diferencia entre el valor presente de la pata flotante (\(PV_{float}\)) y el valor presente de la pata fija (\(PV_{fixed}\)):

\[ V_{swap} = PV_{float} − PV_{fixed} \]

donde:

\[ PV_{fixed} = N K \sum_{i=1}^n \tau_i DF_i \]

\(N\): Nominal,

\(K\): Tasa fija acordada,

\(\tau_i\): Fracción de año para el periodo \(i\).

\(DF(T_i)\): Factor de descuento del pago \(i\).

\[ PV_{float} = N \sum_{i=1}^n F(t_j, T_j) \delta_i DF_j \]

\(F(t_j, T_j)\): tasa Forward entre \(t_j\) y \(T_j\).

\(\delta_j\): Fracción de año de la parte flotante.

\(DF_j\): Factor de descuento del pago \(j\)


  1. El Entorno (Settings): ql.Settings.instance().evaluationDate = today. Especificar la fecha de valoración.

  2. Crear Índice subyacente (ql.Euribor6M).

  3. ql.Schedule: dos, un Swap suele tener dos frecuencias distintas. Es muy común:

  • Pata Fija: Anual (pagas una vez al año).
  • Pata Flotante: Semestral (o Trimestral), coincidiendo con la frecuencia del índice (Euribor 6M o 3M).


  1. El Instrumento (VanillaSwap)

Definimos si somos Payer (pagamos fijo) o Receiver (recibimos fijo), nominal, schedules de la pata fija y variable, índice subyacente y el spread.

ql.VanillaSwap.Payer: “Payer of Fixed”. Por tanto, pagamos el 3% fijo y cobramos Euribor. Alternativamente ql.VanillaSwap.Receiver ``

  1. PricingEngine

ql.DiscountingSwapEngine al que le pasamos la curva de descuento


  1. Resultados

A diferencia del bono, donde miramos el precio o la TIR, en el Swap lo más importante suele ser el Fair Rate.

fairRate(): Es la tasa fija que haría que el NPV del swap fuera exactamente CERO hoy.

fairSpread(): Es el spread que añadido al EURIBOR 6M haría el NPV del swap CERO.

Otros tipos de Swaps:

Valorar Swaps usando Quantlib

Cross Currency Swaps

import QuantLib as ql

# --- PASO 1: Fecha de valoración ---
today = ql.Date(15, 2, 2026)
ql.Settings.instance().evaluationDate = today
calendar = ql.TARGET()

# --- PASO 2: Curva e Índices ---
rate = 0.03
curve_day_count = ql.Actual365Fixed()

curve_handle = ql.YieldTermStructureHandle(
    ql.FlatForward(today, rate, curve_day_count)
)

# 2.2 El Índice (Euribor 6M)
euribor6m = ql.Euribor6M(curve_handle)

# --- PASO 3: El Contrato (Swap) ---
start_date = ql.Date(17, 2, 2026)  # Spot (T+2)
maturity   = ql.Date(17, 2, 2031)  # 5 años
nominal    = 10_000_000.0          # 10 Millones

# 3.1 Calendario Pata Fija (Anual)
fixed_schedule = ql.MakeSchedule(
    effectiveDate=start_date,
    terminationDate=maturity,
    tenor=ql.Period(ql.Annual),
    calendar=calendar,
    convention=ql.ModifiedFollowing,
    terminationDateConvention=ql.ModifiedFollowing,
    rule=ql.DateGeneration.Forward,
    endOfMonth=False)

# 3.2 Calendario Pata Flotante (Semestral, igual que el Euribor 6M)
float_schedule = ql.MakeSchedule(
    effectiveDate=start_date,
    terminationDate=maturity,
    tenor=ql.Period(ql.Semiannual),
    calendar=calendar,
    convention=ql.ModifiedFollowing,
    terminationDateConvention=ql.ModifiedFollowing,
    rule=ql.DateGeneration.Forward,
    endOfMonth=False
)

# --- PASO 5: Construir el Swap ---
fixed_rate = 0.03  # Pagamos fijo al 3%
spread = 0.0       # Spread sobre Euribor

ir_swap = ql.VanillaSwap(
    ql.VanillaSwap.Payer,  # Nosotros PAGAMOS fijo, RECIBIMOS flotante
    nominal,
    fixed_schedule,
    fixed_rate,
    ql.Thirty360(ql.Thirty360.BondBasis), # DayCount Fijo
    float_schedule,
    euribor6m,
    spread,
    ql.Actual360()                        # DayCount Flotante
)

# --- PASO 5: Pricing Engine ---
engine = ql.DiscountingSwapEngine(curve_handle)
ir_swap.setPricingEngine(engine)

# --- RESULTADOS ---
print(f"NPV (Valor Presente Neto): {ir_swap.NPV():,.2f}")
print(f"Fair Rate (Tasa de mercado): {ir_swap.fairRate():.4%}")
print(f"Fair Spread: {ir_swap.fairSpread():.4%}")

# Análisis de las patas por separado (opcional)
print(f"NPV Pata Fija (lo que pago): {ir_swap.fixedLegNPV():,.2f}")
print(f"NPV Pata Flotante (lo que recibo): {ir_swap.floatingLegNPV():,.2f}")

En el ejemplo anterior, si la curva de tipos sube al 4%, ¿gano o pierdo dinero?

  • Gano
  • Pierdo

Como soy Payer (pago fijo al 3% y recibo variable), si los tipos suben al 4%, recibiré pagos variables más altos mientras sigo pagando mi fijo al 3%. Por lo tanto NPV subirá.

Valoración de derivados financieros

Agenda:

  1. Introducción a la programación en Python

  2. Python

  3. QuantLib

  4. Valoración de derivados financieros

  5. Modelo multifactorial de riesgo de crédito

Valoración de derivados: moneda

Imaginemos que queremos valorar y estudiar los riesgos asociados a un contrato financiero cuyo valor final depende del resultado del lanzamiento de la moneda (\(\omega\)) pagamos un 1€ si sale cara y recibimos 1€ si sale cruz. Nosotros como creadores de mercado (market-makers) ofrecemos precios de compra o venta. El espacio muestral es \(\Omega = {H, T}\) (Cara, Cruz).

El primer paso es definir el Payoff (\(V\)), una variable aleatoria al final en \(t_1\):

  • Si sale Cara (\(\omega = H\)): \(V(H) = -1\) (Pagas 1 euro).
  • Si sale Cruz (\(\omega = T\)): \(V(T) = +1\) (Recibes 1 euro).

El valor teórico del contrato en \(t_0\) (\(V_0\)) se define como la esperanza matemática de sus flujos de caja futuros, descontados a la tasa libre de riesgo (\(r\)).

\[ V_0=\frac{1}{1 + r} E[V]=\frac{1}{1 + r} \left[p(H) · V(H) + p(T) · V(C) \right] = \frac{1}{1 + r} \left[p(H) · (-1) + p(T) · (+1) \right] = \frac{p(T) - p(H)}{(1 + r)} = 0 \]

¿Todo lo que me paguen por encima de su valor teórico es beneficio? ¿Cuales son los riesgos asociados a este contrato?

  • Mercado: el resultado de la variable subyacente no sea favorable.
  • Concentración: muchos contratos con el mismo signo (corto o largo).
  • Contraparte: siendo el resultado favorable, el comprador no pagua.
  • Wrong Way Risk: correlación entre la exposición y la calidad crediticia.
  • Modelo: y si la formulación no es correcta y la probabilidad de cara no es 50%.

Valoración: opcion europea (1/6)

Cambiamos el contrato, ahora compramos una Opción Call Europea sobre una acción (\(S_t\)) con precio de ejercicio (Strike) de 100€ y vencimiento en 1 año. El primer paso es definir el Payoff (\(V\)), una variable aleatoria al vencimiento en \(t_1\):

  • Si en \(t=1\) la acción vale mas de 100€, recibire \(S_1 - 100\).
  • Si en \(t=1\) la acción vale menos de 100€, no recibire nada.

Por lo tanto el payoff en el vencimiento es \(max(S_1 - K, 0)\). ¿Pero cuanto vale este contrato hoy?

Al contrario que con la moneda, no conocemos la distribución de probabildad del activo subyacente… Por lo tanto, nos toca hacer una asunción. Por ejemplo, supongamos que modelizamos \(S_t\) a través de un “Movimiento Browniano Geométrico”, su ecuación diferencial seria:

\[ dS_t = \mu S_t dt + \sigma S_t dW_t \]

donde (\(S_t\)) es el precio del activo, (\(\mu\)) es la deriva o “drift”, (\(\sigma\)) is the volatility, and (\(W_t\)) is a Wiener process.

El proceso de Wiener se caracteriza por las siguientes propiedades:

  1. \(W_0\) = 0
  2. Los incrementos de \(W\) son independientes
  3. Los incrementos de \(W\) son normales con media 0 y varianza \(u\).

\[ \forall \ u,t \ge 0, \ W_{t + u} − W_t \sim N(0, u) \]

Valoración: opcion europea (2/6)

La ecuación diferencial del movimiento geométrico browniano es una ecuación es continua. Pero podemos aproximarla (discretizarla) a través del método de Euler (Discretización de Euler-Maruyama). Que consiste en aproximar los diferenciales \(dS\), \(dt\), \(dW\) por incrementos pequeños \(\Delta S\), \(\Delta t\) y \(\Delta W\).

Partimos de:

\[ dS_t = \mu S_t dt + \sigma S_t dW_t \]

Y aproximando con el método de Euler:

\[ S_{t + \Delta t} - S_t = \mu S_t \Delta t + \sigma S_t \Delta W_t \]

Reordenando para predecir el siguiente precio.

\[ S_{t + \Delta t} = S_t + \mu S_t \Delta t + S_t \sigma \Delta W_t) \]

Donde el incremento browniano \(\Delta W_t\) se simula como \(\Delta W_t = \sqrt{\Delta t} Z\) con \(Z \sim N(0,1)\):

Disclaimer

Aunque esta fórmula (Euler directa) parece lógica, tiene un problema grave: no garantiza que el precio sea positivo. Si \(\sigma \Delta W_t\) es un número negativo muy grande, \(S_{t + \Delta t}\) puede volverse negativo, lo cual es imposible para una acción.

La solución exacta (Discretización Logarítmica)

Para evitar precios negativos y ser más precisos, aplicamos el Lema de Itô a \(ln⁡(S_t)\). Lo que nos llevaria a la siguiente solución:

\[ S_t = S_0 e^\left((\mu - \frac{1}{2} \sigma^2) \Delta t + \sigma W_t\right) = S_0 e^\left((\mu - \frac{1}{2} \sigma^2) \Delta t + \sigma \Delta t Z \right) \]

Ver: Aplicación procesos estocásticos

Valoración: opcion europea (3/6)

Simulación de un camino del movimiento browniano geométrico aplicando la discretización de Euler-Maruyama.


import numpy as np
import matplotlib.pyplot as plt

# Parameters
S0 = 100      # initial asset price
mu = 0.05     # drift
sigma = 0.2   # volatility
T = 1.0       # time horizon (year)
steps = 252   # trading days in one year
dt = T / steps

# Simulate one GBM path
# np.random.seed(42)
t = np.linspace(0, T, steps)
W = np.random.standard_normal(size=steps)
W = np.cumsum(W) * np.sqrt(dt)
S = S0 * np.exp((mu - 0.5 * sigma**2) * t + sigma * W)

plt.figure(figsize=(8,4))
plt.plot(t, S)
plt.title('Simulated Geometric Brownian Motion Path')
plt.xlabel('Time (years)')
plt.ylabel('Price')
plt.grid(True)
plt.show()

Valoración: opcion europea (4/6)

Simulación de multiples caminos del movimiento browniano geométrico aplicando la discretización de Euler-Maruyama.


import numpy as np
import matplotlib.pyplot as plt

# Parameters
S0 = 100      # initial asset price
mu = 0.05     # drift
sigma = 0.2   # volatility
T = 1.0       # time horizon (year)
steps = 252   # trading days in one year
dt = T / steps

n_paths = 100  # number of simulated paths
t = np.linspace(0, T, steps)
paths = np.zeros((n_paths, steps))

for i in range(n_paths):
    W = np.random.standard_normal(size=steps)
    W = np.cumsum(W) * np.sqrt(dt)
    S = S0 * np.exp((mu - 0.5 * sigma**2) * t + sigma * W)
    paths[i, :] = S

plt.figure(figsize=(10, 5))
for i in range(n_paths):
    plt.plot(t, paths[i, :], lw=0.8, alpha=0.6)

plt.title('Simulated Geometric Brownian Motion Paths')
plt.xlabel('Time (years)')
plt.ylabel('Price')
plt.grid(True)
plt.show()

Valoración: opcion europea (5/6)

Por lo tanto valoraremos el precio de la opción como la esperanza matemática de sus flujos de caja futuros, descontados a la tasa libre de riesgo \(r\).

K = 100      # strike price
r = 0.05     # risk-free rate
N = 10000    # number of paths

payoffs = np.zeros(N)
for i in range(N):
    W = np.random.standard_normal(size=steps)
    W = np.cumsum(W) * np.sqrt(dt)
    S_T = S0 * np.exp((mu - 0.5*sigma**2)*T + sigma * W[-1])
    payoffs[i] = max(S_T - K, 0)

option_price_mc = np.exp(-r * T) * np.mean(payoffs)
print(f"Monte Carlo Price: {option_price_mc:.4f}")


Solución análitica: Black Scholes

La fórmula de Black-Scholes es:

\[ C = S_0 N(d_1) - K e^{-rT} N(d_2) \]

donde(\(N(\cdot)\)) is the standard normal CDF, y

\[ \begin{matrix} d_1 = \frac{\ln(\frac{S_0}/{K}) + (r + \frac{1}{2} \sigma^2) T}{\sigma \sqrt{T}} \\ d_2 = d_1 - \sigma \sqrt{T} \end{matrix} \]


from scipy.stats import norm

d1 = (np.log(S0/K) + (r + 0.5*sigma**2)*T) / (sigma*np.sqrt(T))
d2 = d1 - sigma*np.sqrt(T)
bs_price = S0*norm.cdf(d1) - K*np.exp(-r*T)*norm.cdf(d2)
print(f"Black-Scholes Price: {bs_price:.4f}")

Valoración: opcion europea (6/6)

Con la libreria QuantLib

Cálculo Analitico (Fórmula Black-Scholes)

import QuantLib as ql
import numpy as np

# 1. Configuración de parámetros
S0 = 100.0      # Precio inicial del activo
K = 100.0       # Precio de ejercicio (Strike)
r = 0.05        # Tasa libre de riesgo
sigma = 0.2     # Volatilidad
T = 1.0         # Tiempo hasta vencimiento (años)
div_yield = 0.0 # Rentabilidad por dividendos (asumimos 0)

calculation_date = ql.Date.todaysDate()
maturity_date = calculation_date + ql.Period(int(T*365), ql.Days)

ql.Settings.instance().evaluationDate = calculation_date

# 2. Construcción del Proceso Estocástico (Black-Scholes-Merton)
spot_handle = ql.QuoteHandle(ql.SimpleQuote(S0))
rate_handle = ql.YieldTermStructureHandle(ql.FlatForward(calculation_date, r, ql.Actual365Fixed()))
div_handle = ql.YieldTermStructureHandle(ql.FlatForward(calculation_date, div_yield, ql.Actual365Fixed()))
vol_handle = ql.BlackVolTermStructureHandle(ql.BlackConstantVol(calculation_date, ql.TARGET(), sigma, ql.Actual365Fixed()))

bsm_process = ql.BlackScholesMertonProcess(spot_handle, div_handle, rate_handle, vol_handle)

# 3. Definición de la Opción
payoff = ql.PlainVanillaPayoff(ql.Option.Call, K)
exercise = ql.EuropeanExercise(maturity_date)
european_option = ql.EuropeanOption(payoff, exercise)

# 4. CÁLCULO ANALÍTICO (Fórmula Black-Scholes)
european_option.setPricingEngine(ql.AnalyticEuropeanEngine(bsm_process))
price_analytic = european_option.NPV()

print(f"Precio Black-Scholes: {price_analytic:.6f}")

Calculo Numerico (Monte Carlo):

import QuantLib as ql
import numpy as np

# 1. Configuración de parámetros
S0 = 100.0      # Precio inicial del activo
K = 100.0       # Precio de ejercicio (Strike)
r = 0.05        # Tasa libre de riesgo
sigma = 0.2     # Volatilidad
T = 1.0         # Tiempo hasta vencimiento (años)
div_yield = 0.0 # Rentabilidad por dividendos (asumimos 0)

calculation_date = ql.Date.todaysDate()
maturity_date = calculation_date + ql.Period(int(T*365), ql.Days)

ql.Settings.instance().evaluationDate = calculation_date

# 2. Construcción del Proceso Estocástico (Black-Scholes-Merton)
spot_handle = ql.QuoteHandle(ql.SimpleQuote(S0))
rate_handle = ql.YieldTermStructureHandle(ql.FlatForward(calculation_date, r, ql.Actual365Fixed()))
div_handle = ql.YieldTermStructureHandle(ql.FlatForward(calculation_date, div_yield, ql.Actual365Fixed()))
vol_handle = ql.BlackVolTermStructureHandle(ql.BlackConstantVol(calculation_date, ql.TARGET(), sigma, ql.Actual365Fixed()))

bsm_process = ql.BlackScholesMertonProcess(spot_handle, div_handle, rate_handle, vol_handle)

# 3. Definición de la Opción
payoff = ql.PlainVanillaPayoff(ql.Option.Call, K)
exercise = ql.EuropeanExercise(maturity_date)
european_option = ql.EuropeanOption(payoff, exercise)

# 4. CÁLCULO NUMÉRICO (Monte Carlo)
steps = 1
N = 100000 # Número de simulaciones

mc_engine = ql.MCEuropeanEngine(bsm_process, "PseudoRandom", timeSteps=steps, requiredSamples=N)
european_option.setPricingEngine(mc_engine)
price_mc = european_option.NPV()

print(f"Precio Monte Carlo: {price_mc:.6f}")

Modelo multifactorial de riesgo de crédito

Agenda:

  1. Introducción a la programación en Python

  2. Python

  3. QuantLib

  4. Valoración de derivados financieros

  5. Modelo multifactorial de riesgo de crédito

Modelo de Merton (1/3)

(Merton, 1974) introdujo los modelos estructurales de riesgo de crédito. La idea básica es ver el default de una empresa como un problema de opciones sobre el valor de sus activos totales. Considerando:

  • \(A_t\): valor de los activos de la empresa en \(t\).
  • \(D\): valor nominal de la deuda que vence en el horizonte \(T\).
  • \(E_t\): valor de mercado del equity.


¿Cual es la condición de default?

\[ A_T < D \]

En ese caso, los accionistas entregan la empresa a los bonistas y su payoff es cero. Por el contrario, si \(A_t \ge D\), el payoff de los accionistas es \(A_T − D\).

El equity es una call sobre los activos con strike \(D\).

Modelo de Merton (2/3)

El supuesto es que los valor de los activos \(A_t\) siguen un movimiento browniano geométrico:

\[ \frac{dA_t}{A_t} = \mu_A dt + \sigma_A dW_t \]

https://dangulo.shinyapps.io/stochastic_processes/

Modelo de Merton (3/3)

El supuesto es que los valor de los activos \(A_t\) siguen un movimiento browniano geométrico:

\[ \frac{dA_t}{A_t} = \mu_A dt + \sigma_A dW_t \]

https://dangulo.shinyapps.io/merton_model/

Bajo los supuestos de Black–Scholes–Merton, \(A_T\) sigue una distribución lognormal. Por lo tanto podemos escribir la probabilidad de default (bajo la medida elegida) como:

\[ PD = \]

Contacto

References

Ametrano, F. M., & Bianchetti, M. (2013). Everything you always wanted to know about multiple interest rate curve bootstrapping but were afraid to ask. SSRN Electronic Journal, (2219548). https://papers.ssrn.com/sol3/papers.cfm?abstract_id=2219548
Merton, R. C. (1974). On the pricing of corporate debt: The risk structure of interest rates. The Journal of Finance, 29(2), 449–470. https://doi.org/10.1111/j.1540-6261.1974.tb03058.x
Musashi, M. (2018). El libro de los cinco anillos (V. G. Díez, Tran.). Ediciones Tutor.
Python Software Foundation. (2025). Python 3.14.0 documentation. Python Software Foundation. https://docs.python.org/es/3/contents.html
StackOverflow. (2025). Technology: 2025 Stack Overflow Developer Survey. https://survey.stackoverflow.co/2025/technology/